Table

Semantic tabular layout for data-dense views — user lists, invoices, reports, admin panels. Cells are slots: drop any letbe-ds component (badge, avatar, button, input, switch, chip) inside a .lb-table__cell. Every th and td carries that class — it is the styling contract, and bare cells render as an unstyled native table. Three sizes, alignment per cell, opt-in zebra striping, sticky header, row selection with select-all + indeterminate state, and sortable columns that dispatch a lb-table-sort event.

a11y (built in): real <table> semantics throughout — <th scope="col"> on every header, .lb-visually-hidden names on the checkbox and actions columns, aria-label on icon-only row controls with their glyphs aria-hidden. Sortable headers are a <button data-lb-sort> inside the <th>: LB.Table cycles aria-sort none → ascending → descending, resets the other columns, and swaps the indicator icon per state so direction is never carried by opacity alone. Select-all drives the native indeterminate property, row expansion toggles aria-expanded on a real button, column-resize handles are role="separator" + tabindex="0" with ←/→ resize and double-click reset (the WCAG 2.5.7 non-drag path), and paged or lazily-appended bodies keep aria-rowcount/aria-rowindex plus a polite live region for the result count.

Usage: Put data-lb-table on <table>. Selection checkboxes use data-lb-table-select-all (in <thead>) and data-lb-table-select (one per body row). Sortable headers use <button data-lb-sort="key">. Add data-lb-sort-auto on the table for built-in DOM sorting; otherwise listen for lb-table-sort and sort on your side. Rows added or removed at runtime (pagination, filtering) just work — selection and sorting are delegated; call el._lbTable.refresh() after swapping rows to sync pre-checked state and new sortable headers, and destroy() to fully detach.


Full demo

Canonical data-view arrangement: the .lb-toolbar sits above the table — density control at rest, and the whole bar flips to bulk actions (.lb-toolbar--selecting) when rows are selected, wired via the lb-table-select event. Sortable columns (Name, Amount) re-order in place (data-lb-sort-auto).

0 selected
Role Status Row actions
Avery Green avery.green@letbe.design
Designer Active $1,240
Noor Ibrahim noor.i@letbe.design
Engineer Invited —
Kai Tanaka kai.t@letbe.design
Product Active $2,400
Luca Rossi luca.r@letbe.design
— Inactive $520
Priya Shah priya.s@letbe.design
Operations Active $3,150

Search, filters + pagination

The client-side data-view flow: a debounced search plus a Filters popover narrow the row set, applied filters render as removable pills in a .lb-filter-bar (the × is the removal idiom, same as Multi-Select pills), pagination slices the result, and refresh() re-syncs selection after every render. Partial-DOM a11y per APG: aria-rowcount on the table, aria-rowindex per row, and the result count is a polite live region. In real apps, swap the filter/slice step for a server query — the chrome stays identical.

Role
Status
Filters:
Name Role Status

Status with icons

Badges accept a .lb-badge__icon slot (16px at medium, 12px at small). Useful when a row needs an immediately-legible status — money flow, delivery state, approval — at a glance. Example below: a transactions view with direction + status columns.

Merchant Date Direction Status Amount
Card payout Apr 18, 2026 Received Confirmed +$2,450.00
Figma subscription Apr 17, 2026 Sent Pending −$45.00
Expired card charge Apr 16, 2026 Sent Failed −$12.99
April salary Apr 15, 2026 Received Confirmed +$8,400.00

Missing cell values

When data loaded successfully but a specific field is null/empty, use <span class="lb-empty-value" aria-label="No data">—</span> — renders a muted em-dash so the row doesn't look broken or still loading. Different from skeleton (data hasn't arrived yet) and empty state row (nothing in the whole table). The em-dash is the conventional marker for a field that has no value.

Name Email Role Last active Deals closed
Avery Green avery@letbe.design Designer 2h ago 12
Noor Ibrahim — Engineer Yesterday —
Kai Tanaka kai@letbe.design — — 7

Sizes

Three row densities. Default is medium. Use .lb-table--sm for dense dashboards and .lb-table--lg for light settings views.

Density toggle — user-switchable, persisted

The data-view pattern: a Segmented control swaps the size class and remembers the choice. Wire lb-segmented-change → toggle --sm/--lg; persist per view.

NameRoleAmount
Avery GreenDesigner$1,240
Noor IbrahimEngineer$980
Kai TanakaProduct$2,400
Small — .lb-table--sm
NameRoleAmount
Avery GreenDesigner$1,240
Noor IbrahimEngineer$980
Kai TanakaProduct$2,400
Medium (default)
NameRoleAmount
Avery GreenDesigner$1,240
Noor IbrahimEngineer$980
Kai TanakaProduct$2,400
Large — .lb-table--lg
NameRoleAmount
Avery GreenDesigner$1,240
Noor IbrahimEngineer$980
Kai TanakaProduct$2,400

Alignment

Default is --start (text). Use --center for icons/indicators, --end for numbers or dates (tabular-nums is applied so digits line up).

Start Center End (numbers)
Title text OK 1,240.50
Another item Warn 980.00
Third row Fail 2,400.75

Cell content — slots

Cells accept any component. Below: avatar + text stack, icon + text, badge, chip, switch, input, link. Wrap grouped content with .lb-table__stack (vertical) or .lb-table__inline (horizontal) helpers. Secondary text uses .lb-table__caption.

Avatar + stack Badge Chip Switch Input Link
Avery Green Designer
Active View profile
Noor Ibrahim Engineer
Pending Details

Row states

Hover highlight (on .lb-table--hoverable), persistent selection (.lb-table__row--selected, auto-applied when the row checkbox is checked), and keyboard focus outline (for row-click patterns).

State Description
Default Hover me to see the highlight.
Selected Row background picks up --lb-action-bg-selected when its checkbox is ticked.
Keyboard focus Tab to this row — outlines with the focus ring. Use for clickable rows.

Zebra striping (opt-in)

Add .lb-table--striped for alternating rows. Off by default — cleaner dashboards look tidier without it; long financial tables read better with it.

NameRoleAmount
Avery GreenDesigner$1,240
Noor IbrahimEngineer$980
Kai TanakaProduct$2,400
Luca RossiQA$520
Priya ShahOperations$3,150

Sticky header

Split the table into two — one for the header, one for the body — so the vertical scrollbar lives only along the rows area, never alongside the pinned <thead>. Both tables get .lb-table--sticky (which applies table-layout: fixed so columns align by default). For unequal column widths, add matching <colgroup> to both. This demo also opts into column resize: data-lb-table-resize on the wrap keeps both split tables' colgroups in sync — drag a header edge, use ←/→ on the focused handle, or double-click the edge to reset (the WCAG 2.5.7 non-drag path). And it demonstrates chunked lazy-append: 200 rows load 30 at a time as you near the scroller's end (aria-rowcount announces the real total; async consumers append skeleton rows while fetching). That's the DS-side scale boundary — for 100k-row sets bring a windowed virtualizer (TanStack Virtual, Clusterize) per the pluggable-engines rule.

Name Role Status Amount

Row expansion + side peek

Two ways to reveal a record's depth: expansion — a data-lb-table-expand button toggles the adjacent .lb-table__detail-row in place (aria-expanded, chevron rotates, detail rows travel with their parent through sorting; not compatible with --striped) — and side peek — the row's end action opens a right lb-sheet with the record while the table stays in context. Dispatches lb-table-expand.

Expand Role Amount Details
Avery Green Designer $1,240
Avery Green — joined Jan 12, 2025 · Rotterdam · 312 sessions. Owns the letbe-ds component gallery and the Figma library bindings.
Noor Ibrahim Engineer $980
Noor Ibrahim — joined Mar 3, 2025 · Stockholm · 1,204 sessions. Maintains the token build pipeline and the theme editor.
Kai Tanaka Product $2,400
Kai Tanaka — joined Jun 21, 2025 · Osaka · 86 sessions. Runs the Stage-5 template program roadmap.

Member

Pinned first column

Wide tables scroll horizontally inside the wrap while .lb-table--pin-first keeps the identity column sticky at the left edge (P&P: as important for horizontal scroll as the fixed header is for vertical). Base background is opaque, row states (hover, selected) still paint through, and a hairline marks the pin boundary. Never collapse a table to display: block for small screens — this is the sanctioned responsive strategy.

Member Role Team Location Joined Last active Sessions Amount
Avery GreenDesignerCoreRotterdamJan 12, 2025Today312$1,240
Noor IbrahimEngineerPlatformStockholmMar 3, 2025Yesterday1,204$980
Kai TanakaProductGrowthOsakaJun 21, 20252 days ago86$2,400

Loading (skeleton)

Set aria-busy="true" on <tbody> and fill cells with .lb-skeleton .lb-skeleton--text. Mirror the final column structure so the layout doesn't jump when data arrives.

NameRoleStatusAmount

Empty state

A single row spanning all columns via colspan. Include a hint for what to do next.

NameRoleStatusAmount

No members yet

Invite a teammate to get started.