Mobile Cards Table
Switch the Mobile/Desktop tab below — each row becomes its own card with labeled fields once the container narrows past 768px, no horizontal scrolling required. It's a container query, not a viewport breakpoint, so it works wherever the table is embedded — no separate mobile component, no JS media-query hook.
npx shadcn add https://shad-table.dev/r/mobile-cards-table.json| Order | Customer | Date | Status | Total |
|---|---|---|---|---|
| #1042 | Ava Thompson | Aug 12, 2026 | Paid | $284.00 |
| #1041 | Liam Chen | Aug 11, 2026 | Pending | $96.50 |
| #1040 | Sofia Patel | Aug 11, 2026 | Paid | $412.20 |
| #1039 | Noah Garcia | Aug 10, 2026 | Refunded | $58.00 |
| #1038 | Mia Johnson | Aug 09, 2026 | Paid | $129.99 |
| #1037 | Ethan Kim | Aug 08, 2026 | Pending | $74.30 |
How it works
1.Every table element gets a display override, not a second layout
There's no separate mobile component and no JS media-query hook — the same <thead>/<tbody>/<tr>/<td> markup renders both views. Below the breakpoint every element is forced to block/flex; above it, the variant puts it back to its native table display (table, table-row-group, table-row, table-cell).
<Table className="block @[48rem]:table"><TableHeader className="hidden @[48rem]:table-header-group"><TableBody className="block ... @[48rem]:table-row-group"><TableRow className="block ... @[48rem]:table-row"><TableCell className="flex ... @[48rem]:table-cell">
2.It's a container query, not a viewport breakpoint
@[48rem] is Tailwind v4's container-query variant, keyed to the nearest ancestor with @container — here, the table's own wrapper div — instead of the browser viewport. A table embedded in a narrow sidebar or split-pane gets the card layout even on a wide desktop screen, and a table given more room stays a table even on a small one.
<div className="@container rounded-md border"><Table className="block @[48rem]:table">{/* @[48rem] reacts to this div's width, not window width */}</Table></div>
3.The header disappears; its text moves onto the cell as a data attribute
hidden @[48rem]:table-header-group removes the header row visually below the breakpoint (screen readers still get context from data-label). Each TableCell carries its own column header as data-label — that's the one piece of information a headerless card still needs per field.
const header = cell.column.columnDef.headerconst label = typeof header === 'string' ? header : undefined<TableCell data-label={label}>
4.A pseudo-element renders the label — and disappears above the breakpoint
before:content-[attr(data-label)] pulls that data-label straight into CSS generated content, so the label renders without any extra DOM node. @[48rem]:before:content-none removes it again once the real <thead> is doing that job instead.
className={cn('flex items-center justify-between gap-4 py-1 @[48rem]:table-cell @[48rem]:py-3','before:content-[attr(data-label)] before:text-xs before:text-muted-foreground','@[48rem]:before:content-none',)}
5.The first column is styled as the card title, not another label:value row
cellIndex === 0 renders the first cell (the order id) without a data-label prefix and in a slightly heavier weight, so each card reads top-to-bottom the way a real mobile UI would: an identifying line first, then attribute rows underneath — instead of every field looking identical.
const isPrimary = cellIndex === 0className={isPrimary? 'mb-1 text-sm font-medium @[48rem]:mb-0 @[48rem]:font-normal': 'before:content-[attr(data-label)] ...'}
6.A Mobile/Desktop tab forces the container width, not the browser
Since the transform keys off @container width, testing it doesn't require resizing the whole window. Mobile pins the demo to a fixed 375px box; Desktop sets a 900px min-width — a min-width, not just 100%, because the docs sidebar can leave the preview pane narrower than the table's own 768px breakpoint. Forcing the simulated width (with horizontal scroll if the pane is tighter) is what a device-preview tab is supposed to do.
<Tabs value={view} onValueChange={(v) => setView(v as View)}><TabsList><TabsTrigger value="mobile">Mobile</TabsTrigger><TabsTrigger value="desktop">Desktop</TabsTrigger></TabsList></Tabs><div className="overflow-x-auto"><divstyle={view === 'mobile'? { width: 375, maxWidth: '100%' }: { minWidth: 900 }}>{children}</div></div>