Competitive comparison
A three-way comparison against approaches rather than named competitors.
Live preview
Source
This exact file renders the preview above.
import { Check, Minus, X } from 'lucide-react'
import { Container } from '@/components/ui/layout'
import { Badge } from '@/components/ui/badge'
import { Table } from '@/components/ui/table'
/**
* Competitive comparison table
*
* A three-way comparison against approaches rather than named competitors —
* which keeps the section honest and stops it ageing badly. Every cell is
* icon + hidden text, so the answer is never conveyed by a glyph alone.
*/
interface Row {
id: string
criterion: string
foundry: 'yes' | 'no' | 'partial'
library: 'yes' | 'no' | 'partial'
scratch: 'yes' | 'no' | 'partial'
}
const rows: Row[] = [
{
id: 'r1',
criterion: 'You own and can edit the source',
foundry: 'yes',
library: 'no',
scratch: 'yes',
},
{
id: 'r2',
criterion: 'Accessible patterns out of the box',
foundry: 'yes',
library: 'yes',
scratch: 'no',
},
{
id: 'r3',
criterion: 'No runtime styling dependency',
foundry: 'yes',
library: 'partial',
scratch: 'yes',
},
{
id: 'r4',
criterion: 'Complete page patterns included',
foundry: 'yes',
library: 'no',
scratch: 'no',
},
{
id: 'r5',
criterion: 'Upgrades arrive automatically',
foundry: 'no',
library: 'yes',
scratch: 'no',
},
{ id: 'r6', criterion: 'Time to first screen', foundry: 'yes', library: 'yes', scratch: 'no' },
]
function Answer({ value }: { value: Row['foundry'] }) {
if (value === 'yes') {
return (
<>
<Check className="mx-auto size-4 text-success" aria-hidden="true" />
<span className="sr-only">Yes</span>
</>
)
}
if (value === 'partial') {
return (
<>
<Minus className="mx-auto size-4 text-warning" aria-hidden="true" />
<span className="sr-only">Partly</span>
</>
)
}
return (
<>
<X className="mx-auto size-4 text-ink-subtle" aria-hidden="true" />
<span className="sr-only">No</span>
</>
)
}
export default function VersusTableComparison() {
return (
<section className="border-b border-line bg-canvas py-section">
<Container>
<div className="max-w-2xl">
<h2 className="display-type text-2xl font-semibold text-ink-strong sm:text-3xl">
Three ways to get a component set.
</h2>
<p className="mt-3 text-md text-ink-muted">
Compared against approaches rather than named products, so the table stays true as the
market changes.
</p>
</div>
<div className="mt-10">
<Table
caption="Foundry compared with an installed component library and building from scratch"
rows={rows}
rowKey={(row) => row.id}
columns={[
{
key: 'criterion',
header: 'Criterion',
width: '46%',
cell: (row) => <span className="text-ink">{row.criterion}</span>,
},
{
key: 'foundry',
header: (
<span className="flex items-center justify-center gap-2">
Foundry{' '}
<Badge size="sm" tone="accent">
This
</Badge>
</span>
),
align: 'center',
cell: (row) => <Answer value={row.foundry} />,
},
{
key: 'library',
header: 'Installed library',
align: 'center',
cell: (row) => <Answer value={row.library} />,
},
{
key: 'scratch',
header: 'From scratch',
align: 'center',
cell: (row) => <Answer value={row.scratch} />,
},
]}
/>
</div>
<p className="mt-4 text-xs text-ink-subtle">
Foundry deliberately does not upgrade automatically. Owning the source means owning the
changes.
</p>
</Container>
</section>
)
}
components/blocks/sections/comparison/versus-table.tsx
import { Check, Minus, X } from 'lucide-react'
import { Container } from '@/components/ui/layout'
import { Badge } from '@/components/ui/badge'
import { Table } from '@/components/ui/table'
/**
* Competitive comparison table
*
* A three-way comparison against approaches rather than named competitors —
* which keeps the section honest and stops it ageing badly. Every cell is
* icon + hidden text, so the answer is never conveyed by a glyph alone.
*/
interface Row {
id: string
criterion: string
foundry: 'yes' | 'no' | 'partial'
library: 'yes' | 'no' | 'partial'
scratch: 'yes' | 'no' | 'partial'
}
const rows: Row[] = [
{
id: 'r1',
criterion: 'You own and can edit the source',
foundry: 'yes',
library: 'no',
scratch: 'yes',
},
{
id: 'r2',
criterion: 'Accessible patterns out of the box',
foundry: 'yes',
library: 'yes',
scratch: 'no',
},
{
id: 'r3',
criterion: 'No runtime styling dependency',
foundry: 'yes',
library: 'partial',
scratch: 'yes',
},
{
id: 'r4',
criterion: 'Complete page patterns included',
foundry: 'yes',
library: 'no',
scratch: 'no',
},
{
id: 'r5',
criterion: 'Upgrades arrive automatically',
foundry: 'no',
library: 'yes',
scratch: 'no',
},
{ id: 'r6', criterion: 'Time to first screen', foundry: 'yes', library: 'yes', scratch: 'no' },
]
function Answer({ value }: { value: Row['foundry'] }) {
if (value === 'yes') {
return (
<>
<Check className="mx-auto size-4 text-success" aria-hidden="true" />
<span className="sr-only">Yes</span>
</>
)
}
if (value === 'partial') {
return (
<>
<Minus className="mx-auto size-4 text-warning" aria-hidden="true" />
<span className="sr-only">Partly</span>
</>
)
}
return (
<>
<X className="mx-auto size-4 text-ink-subtle" aria-hidden="true" />
<span className="sr-only">No</span>
</>
)
}
export default function VersusTableComparison() {
return (
<section className="border-b border-line bg-canvas py-section">
<Container>
<div className="max-w-2xl">
<h2 className="display-type text-2xl font-semibold text-ink-strong sm:text-3xl">
Three ways to get a component set.
</h2>
<p className="mt-3 text-md text-ink-muted">
Compared against approaches rather than named products, so the table stays true as the
market changes.
</p>
</div>
<div className="mt-10">
<Table
caption="Foundry compared with an installed component library and building from scratch"
rows={rows}
rowKey={(row) => row.id}
columns={[
{
key: 'criterion',
header: 'Criterion',
width: '46%',
cell: (row) => <span className="text-ink">{row.criterion}</span>,
},
{
key: 'foundry',
header: (
<span className="flex items-center justify-center gap-2">
Foundry{' '}
<Badge size="sm" tone="accent">
This
</Badge>
</span>
),
align: 'center',
cell: (row) => <Answer value={row.foundry} />,
},
{
key: 'library',
header: 'Installed library',
align: 'center',
cell: (row) => <Answer value={row.library} />,
},
{
key: 'scratch',
header: 'From scratch',
align: 'center',
cell: (row) => <Answer value={row.scratch} />,
},
]}
/>
</div>
<p className="mt-4 text-xs text-ink-subtle">
Foundry deliberately does not upgrade automatically. Owning the source means owning the
changes.
</p>
</Container>
</section>
)
}
components/ui/table.tsx
import type { ReactNode } from 'react'
import { cn } from '@/lib/cn'
/**
* Table
*
* A real `<table>` with `<caption>`, scoped headers and semantic rows — the
* only markup screen readers can navigate cell by cell.
*
* Two responsive strategies are supported, because neither works everywhere:
*
* `scroll` keeps the grid and puts it in a labelled, keyboard-focusable
* scroll region (a scrollable area must be reachable by keyboard).
* `stack` collapses each row into a labelled card below `md`, which reads
* better for short, wide records.
*/
export interface Column<Row> {
key: string
header: ReactNode
/** Cell renderer. Receives the row and its index. */
cell: (row: Row, index: number) => ReactNode
align?: 'start' | 'center' | 'end'
/** Hides the column below `md` in scroll mode. */
hideOnMobile?: boolean
width?: string
}
export interface TableProps<Row> {
caption: string
/** Hides the caption visually while leaving it for assistive tech. */
hideCaption?: boolean
columns: Array<Column<Row>>
rows: Row[]
rowKey: (row: Row, index: number) => string
responsive?: 'scroll' | 'stack'
empty?: ReactNode
className?: string
density?: 'compact' | 'default'
}
const alignClass = { start: 'text-left', center: 'text-center', end: 'text-right' } as const
export function Table<Row>({
caption,
hideCaption = true,
columns,
rows,
rowKey,
responsive = 'scroll',
empty,
className,
density = 'default',
}: TableProps<Row>) {
if (rows.length === 0 && empty) {
return <div className={className}>{empty}</div>
}
const cellPadding = density === 'compact' ? 'px-3 py-1.5' : 'px-4 py-2.5'
const table = (
<table className="w-full border-collapse text-sm">
<caption className={cn('text-left text-xs text-ink-muted', hideCaption ? 'sr-only' : 'pb-3')}>
{caption}
</caption>
<thead>
<tr className="border-b border-line bg-surface-sunken">
{columns.map((column) => (
<th
key={column.key}
scope="col"
style={column.width ? { width: column.width } : undefined}
className={cn(
'label-caps text-ink-muted',
cellPadding,
alignClass[column.align ?? 'start'],
responsive === 'scroll' && column.hideOnMobile && 'hidden md:table-cell',
)}
>
{column.header}
</th>
))}
</tr>
</thead>
<tbody>
{rows.map((row, index) => (
<tr
key={rowKey(row, index)}
className="border-b border-line-subtle last:border-0 hover:bg-surface-sunken/60"
>
{columns.map((column) => (
<td
key={column.key}
className={cn(
'text-ink',
cellPadding,
alignClass[column.align ?? 'start'],
responsive === 'scroll' && column.hideOnMobile && 'hidden md:table-cell',
)}
>
{column.cell(row, index)}
</td>
))}
</tr>
))}
</tbody>
</table>
)
if (responsive === 'stack') {
return (
<div className={className}>
{/* Below md: one labelled card per record. */}
<ul className="flex flex-col gap-2 md:hidden">
{rows.map((row, index) => (
<li key={rowKey(row, index)} className="rounded-lg border border-line bg-surface p-3">
<dl className="flex flex-col gap-1.5">
{columns.map((column) => (
<div key={column.key} className="flex items-baseline justify-between gap-3">
<dt className="label-caps shrink-0 text-ink-subtle">{column.header}</dt>
<dd className="min-w-0 text-right text-sm text-ink">
{column.cell(row, index)}
</dd>
</div>
))}
</dl>
</li>
))}
</ul>
<div className="hidden overflow-hidden rounded-lg border border-line md:block">{table}</div>
</div>
)
}
return (
<div className={cn('overflow-hidden rounded-lg border border-line', className)}>
<div
tabIndex={0}
role="region"
aria-label={caption}
className="thin-scrollbar overflow-x-auto focus-visible:outline-2 focus-visible:-outline-offset-2"
>
{table}
</div>
</div>
)
}
Demo source — adapt to your project. Foundry is not published as a package.
Usage
Comparing against approaches keeps the section honest and stops it ageing badly. Including a row where the product loses is what makes the rest credible.
- Include at least one row where you are not the answer.
- Three states, not two — 'partly' is usually the truthful cell.
Variants and states
Every entry below is a genuine difference in behaviour or layout, and every one of them is visible in the preview above.
- Six criteria
- Three approaches
- Yes, partly and no states
Accessibility
- Triple state
- Yes, partly and no each have a distinct icon and a hidden word.
- Scroll region
- The matrix keeps its grid on narrow screens inside a focusable region.
Foundry implements published ARIA patterns and is tested against them. No WCAG certification is claimed — see the accessibility documentation for what is and is not covered.
Related
All sectionsPricing feature matrix
The detailed plan comparison, with every cell paired with hidden text.
intermediate3 variantsTwo-option comparison
A real either-or decision, with each option stating when it is the wrong choice.
starter3 variantsIncluded and excluded checklist
What the product does and does not do, side by side.
starter3 variants