layout.table

Records with the same fields, compared across: members, invoices, API keys. Write plain thead, tbody, tr, th scope="col" and td inside, with no classes; data-off on a tr greys out a row that is switched off. data-head="bar" sets the header row on a bar and pads the outer columns, for a table that runs edge to edge in a panel with no padding. With more columns than fit on a phone, wrap it in a layout.scroller. data-arrival makes its rows rise into place; give each tr a growing animationDelay to stagger them

Component

Renders

<table>

Profile

Example

Preview
NameRole
Ada LovelaceOwner
Max PlanckMember
<Layout intent="layout.table">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Role</th>
</tr>
</thead>
<tbody>
{members.map((m) => (
<tr key={m.id}>
<td>{m.name}</td>
<td>{m.role}</td>
</tr>
))}
</tbody>
</Layout>

Choosing between neighbours

Find the row that describes your element, and use the intent on the right.

  • If it is

    Use

  • Records with the same fields, compared across: members, invoices, API keys. Write plain thead, tbody, tr, th scope="col" and td inside, with no classes; data-off on a tr greys out a row that is switched off. data-head="bar" sets the header row on a bar and pads the outer columns, for a table that runs edge to edge in a panel with no padding. With more columns than fit on a phone, wrap it in a layout.scroller. data-arrival makes its rows rise into place; give each tr a growing animationDelay to stagger them

    layout.table this page
  • Items read one after another, like a feed or notifications

    layout.list
  • Records compared by the same fields, like a members table

    rather than layout.list
    layout.table this page
  • A real table of data

    rather than layout.index
    layout.table this page

Connections

Declared in the registry, both directions. Where an intent is used in your own code is something check and the gallery see; this site has no app code.

For registry authors

Starter intents. These are the intents prongs add puts in your registry, shown in this site’s theme. Yours are your own registry.ts: rename, restyle and add to them, and see them with prongs add gallery.

What this intent is built from and its entry in components/prongs/registry.ts. You only need this to change how it looks or to add an intent like it.

How layout.table resolves, and its definition

Each layer adds to the one before it; a later class wins over an earlier one that sets the same thing.

  • arrange.table

    Table cells padded and aligned, header cells quiet, a hairline between rows

    w-full border-collapse text-sm [&_th]:px-3 [&_th]:py-2
    [&_th]:text-left [&_th]:font-medium [&_th]:text-surface-muted
    [&_td]:px-3 [&_td]:py-2 [&_td]:align-middle [&_tr]:border-b
    [&_tr]:border-surface-border [&_tbody_tr:last-child]:border-0
    [&_tbody_tr[data-off]]:opacity-50
    [&_tbody_tr:has(.row-open)]:relative
    [&_tbody_tr:has(.row-open)]:cursor-pointer
    [&_tbody_tr:has(.row-open)]:transition-colors
    [&_tbody_tr:has(.row-open):hover]:bg-surface-hover
    [&_tbody_tr:has(.row-open:focus-visible)]:outline-2
    [&_tbody_tr:has(.row-open:focus-visible)]:-outline-offset-2
    [&_tbody_tr:has(.row-open:focus-visible)]:outline-ring
    [&_tbody_tr:has(.row-open)_:is(button,a):not(.row-open)]:relative
    [&_tbody_tr:has(.row-open)_:is(button,a):not(.row-open)]:z-10
  • layout.table

    Records with the same fields, compared across: members, invoices, API keys. Write plain thead, tbody, tr, th scope="col" and td inside, with no classes; data-off on a tr greys out a row that is switched off. data-head="bar" sets the header row on a bar and pads the outer columns, for a table that runs edge to edge in a panel with no padding. With more columns than fit on a phone, wrap it in a layout.scroller. data-arrival makes its rows rise into place; give each tr a growing animationDelay to stagger them

    data-[arrival]:[&_tbody_tr]:animate-[showcase-in_0.5s_ease-out_both]
    motion-reduce:data-[arrival]:[&_tbody_tr]:animate-none
    data-[head=bar]:[&_thead_tr]:border-0
    data-[head=bar]:[&_thead_th]:bg-surface-hover
    data-[head=bar]:[&_thead_th]:py-2.5
    data-[head=bar]:[&_:is(th,td):first-child]:pl-5
    data-[head=bar]:[&_:is(th,td):last-child]:pr-5
    data-[head=bar]:[&_tbody_tr:last-child]:border-b
Final classes
w-full border-collapse text-sm [&_th]:px-3 [&_th]:py-2
[&_th]:text-left [&_th]:font-medium [&_th]:text-surface-muted
[&_td]:px-3 [&_td]:py-2 [&_td]:align-middle [&_tr]:border-b
[&_tr]:border-surface-border [&_tbody_tr:last-child]:border-0
[&_tbody_tr[data-off]]:opacity-50
[&_tbody_tr:has(.row-open)]:relative
[&_tbody_tr:has(.row-open)]:cursor-pointer
[&_tbody_tr:has(.row-open)]:transition-colors
[&_tbody_tr:has(.row-open):hover]:bg-surface-hover
[&_tbody_tr:has(.row-open:focus-visible)]:outline-2
[&_tbody_tr:has(.row-open:focus-visible)]:-outline-offset-2
[&_tbody_tr:has(.row-open:focus-visible)]:outline-ring
[&_tbody_tr:has(.row-open)_:is(button,a):not(.row-open)]:relative
[&_tbody_tr:has(.row-open)_:is(button,a):not(.row-open)]:z-10
data-[arrival]:[&_tbody_tr]:animate-[showcase-in_0.5s_ease-out_both]
motion-reduce:data-[arrival]:[&_tbody_tr]:animate-none
data-[head=bar]:[&_thead_tr]:border-0
data-[head=bar]:[&_thead_th]:bg-surface-hover
data-[head=bar]:[&_thead_th]:py-2.5
data-[head=bar]:[&_:is(th,td):first-child]:pl-5
data-[head=bar]:[&_:is(th,td):last-child]:pr-5
data-[head=bar]:[&_tbody_tr:last-child]:border-b
Definition
TS
'layout.table': {
kind: 'semantic',
description: 'Records with the same fields, compared across: members, invoices, API keys. Write plain thead, tbody, tr, th scope="col" and td inside, with no classes; data-off on a tr greys out a row that is switched off. data-head="bar" sets the header row on a bar and pads the outer columns, for a table that runs edge to edge in a panel with no padding. With more columns than fit on a phone, wrap it in a layout.scroller. data-arrival makes its rows rise into place; give each tr a growing animationDelay to stagger them',
refs: ['arrange.table'],
binding: {
element: 'table',
classes: 'data-[arrival]:[&_tbody_tr]:animate-[showcase-in_0.5s_ease-out_both] motion-reduce:data-[arrival]:[&_tbody_tr]:animate-none data-[head=bar]:[&_thead_tr]:border-0 data-[head=bar]:[&_thead_th]:bg-surface-hover data-[head=bar]:[&_thead_th]:py-2.5 data-[head=bar]:[&_:is(th,td):first-child]:pl-5 data-[head=bar]:[&_:is(th,td):last-child]:pr-5 data-[head=bar]:[&_tbody_tr:last-child]:border-b',
},
notFor: [
{
context: 'items read one after another, like a feed or notifications',
instead: 'layout.list',
},
],
},