Use case featuresDesign system
Site

Use case features

Use-case feature cards, comparison tables, and before-and-after panels.

npx context-ds add use-case-features
On the site

Usage

Use it for

The middle of a use case page, inside a SiteSection under a SectionHeading and SectionHeaderRule. Cards three across when they divide by three, two otherwise, or layout="rows" with each story beside its text. One highlighted column in a comparison.

Avoid

  • More than one highlighted column
  • Colored icon tiles other than brand blue
  • Bold feature titles
  • Tables that squeeze their columns on phones (they scroll)

Dependencies: lucide-react

Example

examples/use-case-features.tsx
import { Clock, Globe, Map as MapIcon } from "lucide-react";import { SectionHeaderRule, SectionHeading, SiteSection } from "@/components/site/section";import { BeforeAfterPanel, BeforeAfterPanels, BeforeAfterStats, UseCaseComparisonTable, UseCaseFeatureCards, UseCaseNote } from "@/components/site/use-case-features";import { DotGlyph } from "@/components/ui/dot-glyph";import { PageMonitorStory, SitemapMonitorStory } from "@/components/visuals/stories/monitor-ways-story";import { WatchStory } from "@/components/visuals/stories/watch-story";const SCRAPE_GLYPH = ["0222220", "2111112", "2122212", "2111112", "2122212", "2111112", "0222220"];export function Site() {  return (    <div className="flex w-full flex-col">      <SiteSection lines>        <SectionHeading title="Watch the pages" accent="that matter." description="Point a monitor at a page, a section or a whole site, and get the change, not the whole page, at your webhook." />        <SectionHeaderRule />        <UseCaseFeatureCards          // One to a row, each story beside its text, so the stories have room in this frame.          layout="rows"          items={[            { title: "Pages", description: "Track one URL for any change to its text, prices or links.", icon: <Globe />, meta: "target: page", tags: ["Exact", "Semantic"], href: "https://context.dev/products/monitor", story: <PageMonitorStory /> },            { title: "Sitemaps", description: "Watch a whole site and hear about every page that is added or removed.", icon: <MapIcon />, meta: "target: sitemap", badge: "New", story: <SitemapMonitorStory /> },            { title: "Schedules", description: "Check every few minutes or once a day, and get a webhook when something moves.", icon: <Clock />, meta: "schedule: 6 hours", story: <WatchStory /> },          ]}        />        <UseCaseNote glyph={<DotGlyph rows={SCRAPE_GLYPH} className="size-9 rounded-control bg-brand p-1.5 text-white" />}>          Every monitor runs on the same scrape API, so the change you get is clean Markdown you can diff or send to a model.        </UseCaseNote>      </SiteSection>      <SiteSection lines>        <SectionHeading title="One API instead of" accent="a scraping stack." />        <SectionHeaderRule />        <UseCaseComparisonTable          label="Context.dev compared with a scraping stack"          columns={[            { key: "diy", header: "Build it yourself", caption: "headless browsers and proxies" },            { key: "context", header: "Context.dev", caption: "one API key", highlight: true },          ]}          rows={[            { feature: "JavaScript pages", diy: "Run and scale your own browsers", context: "Rendered for you" },            { feature: "Blocking", diy: "Rotate proxies and solve challenges", context: "Handled, with retries" },            { feature: "Output", diy: "Raw HTML to clean up", context: "Markdown, HTML, JSON or screenshots" },          ]}        />        <BeforeAfterPanels className="mt-10">          <BeforeAfterPanel tone="before" title="A week of glue code">            <p className="text-body text-fg-secondary">Browsers, proxies and parsers to keep alive, and a queue for the pages that failed.</p>          </BeforeAfterPanel>          <BeforeAfterPanel tone="after" title="One request">            <p className="text-body text-fg-secondary">Send a URL, get the page as Markdown with its links and images.</p>          </BeforeAfterPanel>        </BeforeAfterPanels>        <BeforeAfterStats          accentIndex={2}          stats={[            { label: "Setup", value: "5 min" },            { label: "Pages a minute", value: "1,000+" },            { label: "Infrastructure to run", value: "None" },          ]}        />      </SiteSection>    </div>  );}

Source

components/site/use-case-features.tsx
import * as React from "react";import { ArrowRight } from "lucide-react";import { cn } from "@/lib/utils";// The use case pages' feature sections: white cards in the soft blue ring, each with an optional marker (an icon on a// blue tile, a dot glyph, a badge), a title that can link, a line of description, tags, a code chip with a technical// detail, and a dot story in a demo window at its foot (or beside it, when the cards run one to a row). Then the note// card that can close the section, the comparison table with one column highlighted, and the before and after panels.// The ring is a wrapper rather than a shadow, so its outer edge shares the card's radius. Its negative margin keeps it// from adding layout size.const RING = "-m-2 rounded-card bg-ring-tint p-2";const CARD = "flex h-full min-w-0 flex-col gap-4 rounded-card bg-surface p-6 ring-1 ring-hairline md:p-8";const FOCUS = "outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus-ring";export type UseCaseFeature = {  title: string;  description: React.ReactNode;  href?: string;  /** A line icon, drawn white on a blue tile. */  icon?: React.ReactNode;  /** A dot glyph in place of the icon (the site's animated persona glyphs). */  glyph?: React.ReactNode;  badge?: string;  tags?: readonly string[];  /** A short technical detail shown as a code chip, such as the endpoint or the monitor target. */  meta?: string;  /** A dot story or demo, framed at the foot of the card (or beside it in rows). */  story?: React.ReactNode;};/** The blue tile a feature's or persona's line icon sits on. */function UseCaseIconChip({ children, className }: { children: React.ReactNode; className?: string }) {  return <span className={cn("flex size-11 shrink-0 items-center justify-center rounded-card bg-brand text-white [&_svg]:size-5", className)}>{children}</span>;}/** * A card's demo window: a white window in a blue-tint frame with the window's own radius, and the stage inside it where a * dot story plays, centered at most 26rem wide. `backdrop` takes the ring pattern the site draws behind the story; * `footer` sits under the stage in the same window (the stage then keeps its own height and the footer takes the rest). */function UseCaseDemoFrame({ children, backdrop, footer, className }: { children: React.ReactNode; backdrop?: React.ReactNode; footer?: React.ReactNode; className?: string }) {  const stage = (    <div className="relative isolate flex min-h-88 flex-1 items-center justify-center px-4 py-8 select-none sm:px-9">      {backdrop && (        <div aria-hidden="true" className="pointer-events-none absolute inset-0 -z-10">          {backdrop}        </div>      )}      <div className="relative w-full max-w-104">{children}</div>    </div>  );  return (    <div className={cn("relative mt-auto flex flex-1 flex-col rounded-card bg-brand/10 p-2", className)}>      <div className="flex flex-1 flex-col overflow-hidden rounded-card bg-surface">        {footer ? <div className="flex flex-col">{stage}</div> : stage}        {footer}      </div>    </div>  );}function UseCaseFeatureCard({ feature, row = false, className }: { feature: UseCaseFeature; row?: boolean; className?: string }) {  const { title, description, href, icon, glyph, badge, tags, meta, story } = feature;  const heading = (    <span className="flex items-center gap-2">      {title}      {href && <ArrowRight aria-hidden="true" className="size-4 shrink-0 text-fg-brand" />}    </span>  );  return (    <div data-slot="use-case-feature-card" className={cn(CARD, row && "lg:grid lg:grid-cols-[minmax(0,1fr)_minmax(0,1.4fr)] lg:items-center lg:gap-12", className)}>      <div className={cn("flex flex-col gap-4", row ? "max-w-md" : "flex-1")}>        {(icon || glyph || badge) && (          <div className="flex items-center justify-between gap-4">            {glyph ?? (icon && <UseCaseIconChip>{icon}</UseCaseIconChip>)}            {badge && <span className="rounded-pill bg-brand/10 px-3 py-1 text-body-sm leading-snug text-fg-brand">{badge}</span>}          </div>        )}        <h3 className="text-h4 leading-snug font-normal tracking-normal text-fg">          {href ? (            <a href={href} className={cn("rounded-sm transition-[color] duration-150 ease-out hover:text-fg-brand motion-reduce:transition-none", FOCUS)}>              {heading}            </a>          ) : (            heading          )}        </h3>        <p className="text-body leading-snug text-fg-secondary">{description}</p>        {tags && tags.length > 0 && (          <ul className="flex flex-wrap gap-1.5">            {tags.map((tag) => (              <li key={tag} className="rounded-pill px-3 py-1 text-body-sm leading-snug text-fg-nav ring-1 ring-black/10 ring-inset">                {tag}              </li>            ))}          </ul>        )}        {meta && <code className={cn("w-fit rounded-pill bg-wash-hover px-3 py-1.5 font-mono text-body-sm leading-5 text-fg-nav", !story && "mt-auto")}>{meta}</code>}      </div>      {story && (        <div className={cn("flex flex-col", !row && "mt-4 flex-1")}>          <UseCaseDemoFrame>{story}</UseCaseDemoFrame>        </div>      )}    </div>  );}/** * The cards: three across when they divide by three, two across otherwise, or one to a row (`layout="rows"`) with each * card's story beside its text. */function UseCaseFeatureCards({ items, layout = "grid", className }: { items: readonly UseCaseFeature[]; layout?: "grid" | "rows"; className?: string }) {  const rows = layout === "rows";  return (    <ul data-slot="use-case-feature-cards" className={cn("grid grid-cols-1 gap-6", !rows && (items.length % 3 === 0 ? "lg:grid-cols-3" : "md:grid-cols-2"), className)}>      {items.map((feature) => (        <li key={feature.title} className={RING}>          <UseCaseFeatureCard feature={feature} row={rows} />        </li>      ))}    </ul>  );}/** A closing note under the cards, with a small glyph (or icon tile) standing for the API it points to. */function UseCaseNote({ glyph, children, className }: { glyph?: React.ReactNode; children: React.ReactNode; className?: string }) {  return (    <div data-slot="use-case-note" className={cn(RING, "mt-6", className)}>      <div className="flex items-center gap-4 rounded-card bg-surface p-6 ring-1 ring-hairline md:px-8">        {glyph}        <p className="text-body-lg leading-snug text-fg-secondary">{children}</p>      </div>    </div>  );}export type UseCaseComparisonColumn = { key: string; header: React.ReactNode; caption?: React.ReactNode; highlight?: boolean };export type UseCaseComparisonRow = { feature: React.ReactNode } & Record<string, React.ReactNode>;/** * The comparison table: the features down the left, a column per option, the highlighted one (Context.dev) in brand blue * on a faint blue wash. It scrolls sideways on narrow screens rather than squeezing the columns. */function UseCaseComparisonTable({ columns, rows, label = "Comparison", className }: { columns: readonly UseCaseComparisonColumn[]; rows: readonly UseCaseComparisonRow[]; label?: string; className?: string }) {  return (    <div data-slot="use-case-comparison-table" className={cn(RING, className)}>      <div tabIndex={0} role="region" aria-label={label} className={cn("overflow-x-auto rounded-card bg-surface ring-1 ring-hairline", FOCUS)}>        <table className="w-full min-w-xl border-collapse text-left">          <thead>            <tr className="border-b border-hairline align-bottom">              <td className="w-1/4 px-6 py-5 md:px-8" />              {columns.map((column) => (                <th key={column.key} scope="col" className={cn("px-6 py-5 font-normal md:px-8", column.highlight && "bg-brand/4")}>                  <span className={cn("block text-h5 leading-snug", column.highlight ? "text-fg-brand" : "text-fg")}>{column.header}</span>                  {column.caption && <span className="block text-body-sm leading-snug text-fg-subtle">{column.caption}</span>}                </th>              ))}            </tr>          </thead>          <tbody>            {rows.map((row, i) => (              <tr key={i} className="border-b border-black/6 last:border-0">                <th scope="row" className="px-6 py-5 align-top text-body leading-snug font-normal text-fg md:px-8">                  {row.feature}                </th>                {columns.map((column) => (                  <td key={column.key} className={cn("px-6 py-5 align-top text-body leading-snug md:px-8", column.highlight ? "bg-brand/4 text-fg" : "text-fg-secondary")}>                    {row[column.key]}                  </td>                ))}              </tr>            ))}          </tbody>        </table>      </div>    </div>  );}const PANEL_TONES = {  before: { label: "Without Context.dev", labelClass: "text-fg-muted ring-1 ring-black/10 ring-inset" },  after: { label: "With Context.dev", labelClass: "bg-brand/10 text-fg-brand" },} as const;/** One side of a before and after: a label chip, a title and whatever shows the difference. */function BeforeAfterPanel({ tone, label, title, className, children }: { tone: keyof typeof PANEL_TONES; label?: string; title: React.ReactNode; className?: string; children: React.ReactNode }) {  const preset = PANEL_TONES[tone];  return (    <section aria-label={label ?? preset.label} data-slot="before-after-panel" className={cn("flex min-w-0 flex-col gap-5 rounded-card bg-surface p-5 ring-1 ring-hairline sm:p-6 md:p-7", className)}>      <span className={cn("w-fit rounded-pill px-3 py-1 text-body-sm leading-snug", preset.labelClass)}>{label ?? preset.label}</span>      <h3 className="text-h4 leading-snug font-normal tracking-normal text-fg">{title}</h3>      {children}    </section>  );}function BeforeAfterPanels({ children, className }: { children: React.ReactNode; className?: string }) {  return <div className={cn("grid grid-cols-1 gap-6 lg:grid-cols-2", className)}>{children}</div>;}/** The numbers under a before and after, one of them in brand blue. */function BeforeAfterStats({ stats, accentIndex = 0, className }: { stats: readonly { label: string; value: string }[]; accentIndex?: number; className?: string }) {  return (    <dl className={cn("mt-6 grid grid-cols-1 rounded-card bg-surface ring-1 ring-hairline sm:grid-cols-3", className)}>      {stats.map((stat, i) => (        <div key={stat.label} className={cn("flex flex-col gap-1 px-6 py-5", i > 0 && "border-t border-black/6 sm:border-t-0 sm:border-l")}>          <dt className="text-body-sm leading-snug text-fg-subtle">{stat.label}</dt>          <dd className={cn("text-h3 leading-none tracking-tight", i === accentIndex ? "text-fg-brand" : "text-fg")}>{stat.value}</dd>        </div>      ))}    </dl>  );}export { UseCaseFeatureCards, UseCaseFeatureCard, UseCaseIconChip, UseCaseDemoFrame, UseCaseNote, UseCaseComparisonTable, BeforeAfterPanels, BeforeAfterPanel, BeforeAfterStats };

Reference: src/components/sections/usecases/use-case-landing-page.tsx; src/components/sections/use-cases/use-case-sections.tsx; src/components/sections/usecases/before-after-panels.tsx; src/components/sections/home/product-demo-panel.tsx; src/components/sections/home/story-stage.tsx.