Comparison tableDesign system
Site

Comparison table

Comparison table with a highlighted Context.dev column and text or check-mark cells.

npx context-ds add comparison-table
On the site
Marks

Usage

Use it for

Comparing the product with an alternative: text on a use-case page (your own crawler against the API), marks for an alternatives page's full feature comparison. Mark Context.dev's column with highlight.

Avoid

  • Uppercase or bold heads
  • Red crosses for a missing feature (an en dash)
  • Vertical rules between columns
  • Gray category bands

Dependencies: lucide-react

Example

examples/comparison-table.tsx
import { SiteSection } from "@/components/site/section";import { ComparisonTable } from "@/components/site/comparison-table";// A use-case page's comparison: answers in words, the alternative first and the brand's column last.export function Site() {  return (<SiteSection>    <ComparisonTable      className="w-full"      caption="Context.dev compared with your own crawler"      columns={[        { key: "alternative", header: "Your own crawler", caption: "build and maintain" },        { key: "context", header: "Context.dev", caption: "one API key", highlight: true },      ]}      rows={[        { feature: "JavaScript-heavy pages", alternative: "Run and scale a fleet of headless browsers", context: "Every page rendered in a real browser" },        { feature: "Bot protection", alternative: "Buy proxies, rotate them, and write retry logic", context: "Automatic proxy escalation and retries" },        { feature: "Clean output", alternative: "Write HTML-to-Markdown cleanup for every site", context: "Markdown with the main content only" },        { feature: "Large sites", alternative: "Build queues and workers to fan out", context: "Up to 100,000 pages per batch crawl" },        { feature: "Cost", alternative: "Servers, proxies, and engineering time", context: "1 credit per page, failed pages are free" },      ]}    />  </SiteSection>);}// An alternatives page's full comparison: checks, dashes and short figures under category headings.export function Marks() {  return (<SiteSection>    <ComparisonTable      className="w-full"      variant="marks"      caption="Context.dev compared with Firecrawl"      columns={[        { key: "context", header: "Context.dev", highlight: true },        { key: "firecrawl", header: "Firecrawl" },      ]}      rows={[        { category: "Scraping & Crawling", feature: "Scrape to Markdown or HTML", context: true, firecrawl: true },        { category: "Scraping & Crawling", feature: "JSON extraction against your schema", context: "5 credits", firecrawl: "5 credits" },        { category: "Scraping & Crawling", feature: "Async batch scraping", context: "Up to 100,000 URLs", firecrawl: true },        { category: "Scraping & Crawling", feature: "JSON extraction inside batches", context: false, firecrawl: true },        { category: "Interaction & Search", feature: "Search 10 results with page Markdown", context: "2 credits", firecrawl: "12 credits" },        { category: "Interaction & Search", feature: "Live multi-step browser sessions", context: false, firecrawl: "Billed per minute" },        { category: "Pricing & Billing", feature: "Highlights (relevant passages)", context: "+1 credit", firecrawl: "+4 credits" },        { category: "Pricing & Billing", feature: "Zero data retention", context: "No extra credits", firecrawl: "+1 credit/page" },        { category: "Also Included: Brand & Company Data", feature: "Brand profile by domain, name, email, or ticker", context: true, firecrawl: false },        { category: "Also Included: Brand & Company Data", feature: "Logo CDN for direct embedding (Logo Link)", context: true, firecrawl: false },      ]}      footnote="Prices and features checked May 2026."    />  </SiteSection>);}

Source

components/site/comparison-table.tsx
import * as React from "react";import { Check } from "lucide-react";import { cn } from "@/lib/utils";// The site's comparison tables, both inside a white card in the soft blue glass ring, the brand's own column tinted blue// at 4 percent, and no vertical rules.// - `text` (the use-case pages): column heads at h5 with a small caption under each, hairline rows, and each cell a//   short answer in words, the alternative's muted and the brand's in black.// - `marks` (the alternatives pages' full comparison): compact centered heads, rows grouped under plain category//   headings, and each cell a check, a quiet en dash for no, or a short figure (in blue in the brand's column).export type ComparisonCellValue = boolean | string;export type ComparisonColumn = { key: string; header: string; caption?: string; highlight?: boolean };export type ComparisonRow = { feature: string; category?: string; [key: string]: ComparisonCellValue | undefined };/** A `marks` cell: a check, an en dash for no, or a short figure. */function ComparisonMark({ value, highlight }: { value: ComparisonCellValue | undefined; highlight?: boolean }) {  if (typeof value === "string") return <span className={cn("text-body-sm", highlight ? "font-medium text-fg-brand" : "text-fg-nav")}>{value}</span>;  if (value) {    return (      <span className="inline-flex justify-center">        <Check aria-hidden="true" className={cn("size-5", highlight ? "text-fg-brand" : "text-[var(--ds-color-neutral-50)]")} />        <span className="sr-only">Yes</span>      </span>    );  }  return (    <span className="text-body-sm text-neutral-30">      <span aria-hidden="true">–</span>      <span className="sr-only">No</span>    </span>  );}function ComparisonTable({  columns,  rows,  variant = "text",  caption,  firstColumnHeader,  footnote,  className,}: {  columns: ComparisonColumn[];  rows: ComparisonRow[];  variant?: "text" | "marks";  /** Names the scrolling region and the table for screen readers; the visible title belongs to the section around it. */  caption?: string;  /** The first column's head; `marks` defaults to "Feature", `text` leaves it empty. */  firstColumnHeader?: string;  /** A line under the card, such as when prices were last checked. */  footnote?: React.ReactNode;  className?: string;}) {  const marks = variant === "marks";  const categories = [...new Set(rows.flatMap((row) => (row.category ? [row.category] : [])))];  const groups = categories.length > 0 ? categories.map((category) => ({ category, rows: rows.filter((row) => row.category === category) })) : [{ category: undefined, rows }];  const firstHeader = firstColumnHeader ?? (marks ? "Feature" : undefined);  const last = columns.length - 1;  return (    <div data-slot="comparison-table" className={cn("flex flex-col gap-3", className)}>      {/* The glass ring: a wrapper 8px wider than the card, so its outer edge shares the card's radius. */}      <div className="-m-2 rounded-card bg-ring-tint p-2">        <div tabIndex={0} role="region" aria-label={caption} className="overflow-x-auto rounded-card bg-surface ring-1 ring-hairline outline-none focus-visible:ring-2 focus-visible:ring-focus-ring">          <table className={cn("w-full border-collapse text-left leading-snug", marks ? "min-w-136" : "min-w-160")}>            {caption && <caption className="sr-only">{caption}</caption>}            <thead>              {marks ? (                <tr className="text-body-sm">                  <th scope="col" className="px-5 py-4 font-normal text-fg-nav md:px-8">                    {firstHeader}                  </th>                  {columns.map((col) => (                    <th key={col.key} scope="col" className={cn("w-36 px-4 py-4 text-center md:w-44", col.highlight ? "bg-brand/4 font-medium text-fg-brand" : "font-normal text-fg")}>                      {col.header}                    </th>                  ))}                </tr>              ) : (                <tr className="align-bottom">                  {firstHeader ? (                    <th scope="col" className="w-[28%] px-5 py-5 text-body-sm font-normal text-fg-subtle md:px-8">                      {firstHeader}                    </th>                  ) : (                    <td className="w-[28%] px-5 py-5 md:px-8" />                  )}                  {columns.map((col, i) => (                    <th key={col.key} scope="col" className={cn("px-5 py-5 font-normal", i === last && "md:px-8", col.highlight && "bg-brand/4")}>                      <span className={cn("block text-h5", col.highlight ? "text-fg-brand" : "text-fg")}>{col.header}</span>                      {col.caption && <span className="block text-body-sm text-fg-subtle">{col.caption}</span>}                    </th>                  ))}                </tr>              )}            </thead>            {groups.map((group) => (              <tbody key={group.category ?? "rows"}>                {group.category && (                  <tr className="border-t border-hairline">                    <th scope="colgroup" colSpan={columns.length + 1} className="px-5 pt-6 pb-2 text-body-sm font-medium text-fg md:px-8">                      {group.category}                    </th>                  </tr>                )}                {group.rows.map((row) =>                  marks ? (                    <tr key={row.feature} className="border-t border-wash-pill">                      <th scope="row" className="px-5 py-3.5 text-body font-normal text-fg-nav md:px-8">                        {row.feature}                      </th>                      {columns.map((col) => (                        <td key={col.key} className={cn("px-4 py-3.5 text-center", col.highlight && "bg-brand/4")}>                          <ComparisonMark value={row[col.key]} highlight={col.highlight} />                        </td>                      ))}                    </tr>                  ) : (                    <tr key={row.feature} className="border-t border-hairline">                      <th scope="row" className="px-5 py-4 text-body font-normal text-fg md:px-8">                        {row.feature}                      </th>                      {columns.map((col, i) => (                        <td key={col.key} className={cn("px-5 py-4 text-body", i === last && "md:px-8", col.highlight ? "bg-brand/4 text-fg" : "text-fg-subtle")}>                          {typeof row[col.key] === "boolean" ? <ComparisonMark value={row[col.key]} highlight={col.highlight} /> : row[col.key]}                        </td>                      ))}                    </tr>                  ),                )}              </tbody>            ))}          </table>        </div>      </div>      {footnote && <p className="text-body-sm leading-relaxed text-fg-subtle">{footnote}</p>}    </div>  );}export { ComparisonTable, ComparisonMark };

Reference: src/components/sections/use-cases/use-case-sections.tsx (UseCaseComparison); src/components/sections/alternatives/alternative-comparison-table.tsx.