Comparison table

Us vs typical vendor across six rows, the brand column tinted and marked, rows staggered in on view.

blockversusanycomparisontrust
$npx shadcn@latest add @storekit/comparison-table
comparison-table

Live and interactive. Switch the brand in the frame header to see the same source under another token file.

Installation

$npx shadcn@latest add @storekit/comparison-table
$pnpm dlx shadcn@latest add @storekit/comparison-table
$yarn dlx shadcn@latest add @storekit/comparison-table
$bunx --bun shadcn@latest add @storekit/comparison-table

Source

components/storekit/marketing/comparison-table.tsx
134 lines
"use client"

import * as React from "react"
import { motion, useReducedMotion } from "motion/react"
import { Check, X } from "lucide-react"
import { cn } from "@/components/storekit/lib/cn"
import { Button } from "@/components/storekit/ui/button"
import { useSkBlock } from "@/components/storekit/lib/sk-analytics"

/* "Us vs typical vendor" table. Booleans render as check or x, strings
   render as values. The brand column carries a tinted column background and
   the brand mark; the other column stays muted so the contrast reads at a
   glance. Rows rise in, staggered, when scrolled into view. An optional CTA
   closes the block. */

type Row = { feature: string; us: boolean | string; them: boolean | string }

const SAMPLE: Row[] = [
  { feature: "Third-party lab results for every batch", us: true, them: false },
  { feature: "Price per serving", us: "$1.63", them: "$2.90" },
  { feature: "Added sugar", us: "0g", them: "4g" },
  { feature: "Artificial colors or fillers", us: false, them: true },
  { feature: "Made in the USA", us: true, them: true },
  { feature: "30-day guarantee, even if empty", us: true, them: false },
]

const EASE: [number, number, number, number] = [0.16, 1, 0.3, 1]

function Cell({ v, brand }: { v: boolean | string; brand?: boolean }) {
  if (typeof v === "string") return <span className={cn("text-sm tabular-nums", brand ? "font-semibold text-sk-ink" : "text-sk-muted")}>{v}</span>
  if (!v)
    return (
      <span className="inline-grid h-7 w-7 place-items-center text-sk-muted/70">
        <X size={15} strokeWidth={2} aria-label="No" />
      </span>
    )
  return (
    <span className={cn("inline-grid h-7 w-7 place-items-center rounded-sk-chip", brand ? "bg-sk-accent text-sk-accent-ink" : "bg-sk-ink/[0.06] text-sk-ink/70")}>
      <Check size={15} strokeWidth={2.5} aria-label="Yes" />
    </span>
  )
}

export function ComparisonTable({
  brandName = "Morrow",
  markSrc = "/brands/morrow/mark.webp",
  othersLabel = "Typical brand",
  eyebrow = "Why us",
  title = "The difference is in the scoop.",
  rows = SAMPLE,
  ctaLabel = "Shop Daily Greens",
  onCta,
  className,
}: {
  brandName?: string
  markSrc?: string
  othersLabel?: string
  eyebrow?: string
  title?: string
  rows?: Row[]
  /** Pass an empty string to hide the closing button. */
  ctaLabel?: string
  onCta?: () => void
  className?: string
}) {
  const reduce = useReducedMotion()
  const sk = useSkBlock("marketing", "comparison-table")
  const tint = "bg-sk-accent/[0.06]"
  return (
    <section ref={sk.ref as React.Ref<HTMLElement>} {...sk.attrs} className={cn("w-full px-4 py-16 sm:px-8 sm:py-24", className)}>
      <div className="mx-auto max-w-3xl">
        <p className="text-center font-sk-mono text-[11px] uppercase tracking-[0.14em] text-sk-muted">{eyebrow}</p>
        <h2 className="mt-3 text-center font-sk-display text-3xl font-semibold leading-[1.1] tracking-[-0.02em] sm:text-4xl">{title}</h2>
        <table className="mt-10 w-full table-fixed border-separate border-spacing-0 text-left">
          <colgroup>
            <col className="w-[46%]" />
            <col className="w-[27%]" />
            <col className="w-[27%]" />
          </colgroup>
          <thead>
            <tr>
              <th scope="col" className="pb-4 align-bottom font-sk-mono text-[11px] font-normal uppercase tracking-[0.14em] text-sk-muted">Feature</th>
              <th scope="col" className={cn("rounded-t-sk-card px-2 pb-4 pt-5 text-center align-bottom", tint)}>
                <span className="flex flex-col items-center gap-2">
                  <img src={markSrc} alt="" className="h-8 w-8 object-contain" />
                  <span className="text-sm font-semibold text-sk-ink">{brandName}</span>
                </span>
              </th>
              <th scope="col" className="px-2 pb-4 text-center align-bottom text-sm font-medium text-sk-muted">{othersLabel}</th>
            </tr>
          </thead>
          <motion.tbody
            initial="hidden"
            whileInView="show"
            viewport={{ once: true, amount: 0.3 }}
            variants={{ hidden: {}, show: { transition: { staggerChildren: reduce ? 0 : 0.05 } } }}
          >
            {rows.map((r, i) => {
              const last = i === rows.length - 1
              return (
                <motion.tr
                  key={r.feature}
                  variants={{ hidden: { opacity: 0, y: reduce ? 0 : 8 }, show: { opacity: 1, y: 0, transition: { duration: 0.5, ease: EASE } } }}
                >
                  <th scope="row" className="border-t border-sk-line py-4 pr-3 text-sm font-normal leading-[1.5] text-sk-ink">{r.feature}</th>
                  <td className={cn("border-t border-sk-line px-2 py-4 text-center", tint, last && "rounded-b-sk-card")}>
                    <Cell v={r.us} brand />
                  </td>
                  <td className="border-t border-sk-line px-2 py-4 text-center">
                    <Cell v={r.them} />
                  </td>
                </motion.tr>
              )
            })}
          </motion.tbody>
        </table>
        {ctaLabel ? (
          <div className="mt-10 flex justify-center">
            <Button
              size="lg"
              onClick={() => {
                sk.emit("convert", "comparison_cta")
                onCta?.()
              }}
            >
              {ctaLabel}
            </Button>
          </div>
        ) : null}
      </div>
    </section>
  )
}

Usage

Import from "@/components/storekit/marketing/comparison-table" and pass your product data. The demo above exercises the full prop surface with sample values.

Comparison table, StoreKit