Table
Use table with the current published Askr packages.
@askrjs/ui/table0.0.13@askrjs/themes/table0.0.13
How to use table
Table should use the standard themed surface unless the application is deliberately implementing its own design system on top of @askrjs/ui.
- Import the themed component from @askrjs/themes/components; use @askrjs/ui directly only when building a custom visual system.
- Keep table labels, state, and application actions visible at the call site instead of styling internal DOM nodes.
- Verify keyboard behavior, focus movement or return, disabled state, and both standard themes.
import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow } from '@askrjs/themes/components';
<Table>
<TableHead><TableRow><TableHeaderCell>Name</TableHeaderCell><TableHeaderCell>Status</TableHeaderCell></TableRow></TableHead>
<TableBody>{projects.map((project) => (
<TableRow key={project.id}><TableCell>{project.name}</TableCell><TableCell>{project.status}</TableCell></TableRow>
))}</TableBody>
</Table>Purpose
Table belongs to the ui & components layer. Use it when that layer owns the lifecycle or contract; keep simpler local behavior in state or ordinary components, and move network or request authority to the server boundary.
Install and import
@askrjs/ui/table ships eight unstyled primitives — Table, TableCaption, TableHead, TableBody, TableFoot, TableRow, TableHeaderCell, and TableCell — each wrapping the matching native element (table, caption, thead, tbody, tfoot, tr, th, td) with no attached behavior. @askrjs/themes/table re-exports those same primitives untouched and layers on CSS keyed to data-slot attributes, so the tree you write is identical whichever package you import from. Styling only shows up once you also load @askrjs/themes/default (or another theme) — the JS subpath alone ships no CSS. Note that @askrjs/themes/table, /data-table, and /scroll-area all resolve to the same dist/components.js barrel, so the subpath you pick is mostly documentation convenience; a bundler still tree-shakes what you don't use.
Live examples
A typical table pairs TableCaption for a one-line summary with TableHead/TableHeaderCell for column labels, then TableBody/TableRow/TableCell for data rows, closing with an optional TableFoot for totals. Since every part is a native-element wrapper, you build it the way you'd hand-write a table — map an array to TableRow children and drop a TableCell per field. There's no bundled data-fetching or column-definition API; sorting, filtering, and formatting all live in your own component code around these primitives.
Anatomy
The rendered tree mirrors the native document outline exactly: table > caption? + thead > tr > th* alongside tbody > tr > td*, with an optional tfoot > tr > td*. Table accepts standard table attributes plus asChild, and every child primitive offers the same asChild escape hatch if you need to swap the rendered tag or merge props onto a custom element. There's no implicit wrapper div or portal inserted anywhere — what you write is what lands in the DOM.
State model
None of the Table primitives hold internal state — no sort order, no row selection, no pagination cursor. Sorting a column, tracking an expanded row, or paging results are decisions your surrounding component makes, feeding back into which rows and cells get rendered on the next pass. If you need virtualized rendering for large row counts, reach for Virtual Table instead, which does carry scroll and selection state.
Keyboard and accessibility
Because each part renders its native HTML counterpart, table navigation and screen-reader row/column announcements come from the browser and platform accessibility tree for free — there's no custom ARIA layer to configure. TableHeaderCell renders a real th, so use it for column (and row, where relevant) headers rather than TableCell, or assistive tech loses the header-to-cell relationship. TableCaption is the right place for a short description of the table's contents; screen readers announce it before reading the grid.
Styling and tokens
@askrjs/themes/table styles each part by its data-slot attribute — table, table-caption, table-head, table-body, table-foot — pulling colors, spacing, and radius from the same --ak-* custom properties every other themed component uses, so a table matches the rest of the app automatically. The base table has border-collapse: collapse, a transparent background, and no border of its own; wrap it in Data Table or Card if you want a bordered, elevated surface. Since the CSS is attribute-selector based, styling still applies even when Table is rendered with asChild onto a different element.
API
TableProps extends the native table element's props and adds asChild?: boolean, which requires a single JSXElement child in place of table children and merges the resolved props onto it. TableRowProps, TableCellProps, TableHeaderCellProps, TableHeadProps, TableBodyProps, TableFootProps, and TableCaptionProps follow the same pattern against their respective native elements. None of these types add table-specific props like sortDirection — colSpan, rowSpan, and similar attributes just pass through as ordinary HTML props.
Edge cases
Large row counts will slow down or jank a plain Table because every row is real DOM — there's no windowing built in, so switch to Virtual Table once you're past a few hundred rows at once. If you use asChild on Table itself, the child you pass must already be (or forward props onto) a table-compatible element, or the merged data-slot and ARIA attributes end up somewhere the browser doesn't expect. Empty states aren't handled for you — an empty TableBody renders as a header with no rows — so pair it with your own empty-state markup (Empty from @askrjs/themes fits well) when the row array is empty.
Related pages
See Data Table for a styled bordered container to wrap a Table in. See Virtual Table for the same tabular shape with windowed rendering for large datasets. See Scroll Area if you need a custom-styled scrolling region around a wide or tall table.