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-tablecomparison-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-tableSource
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.
