Table
Displays tabular data with responsive styles.
- v1.0.0
- SSR
- Zoneless
- 0.8 kB gzip
| Invoice | Status | Method | Amount |
|---|---|---|---|
| INV001 | Paid | Credit card | $250.00 |
| INV002 | Pending | PayPal | $150.00 |
| INV003 | Unpaid | Bank transfer | $350.00 |
| INV004 | Paid | Credit card | $450.00 |
| INV005 | Paid | PayPal | $550.00 |
| Total | $1,750.00 | ||
import { Component } from '@angular/core';
import {
Table,
TableBody,
TableCaption,
TableCell,
TableContainer,
TableFooter,
TableHead,
TableHeader,
TableRow,
} from '@/ui/table/table';
/** Recent invoices, named by a caption, with a total in the footer. */
@Component({
selector: 'example-table-demo',
imports: [
Table,
TableBody,
TableCaption,
TableCell,
TableContainer,
TableFooter,
TableHead,
TableHeader,
TableRow,
],
template: `
<div ndTableContainer>
<table ndTable>
<caption ndTableCaption>
A list of your recent invoices.
</caption>
<thead ndTableHeader>
<tr ndTableRow>
<th ndTableHead scope="col" class="w-24">Invoice</th>
<th ndTableHead scope="col">Status</th>
<th ndTableHead scope="col">Method</th>
<th ndTableHead scope="col" class="text-end">Amount</th>
</tr>
</thead>
<tbody ndTableBody>
@for (invoice of invoices; track invoice.id) {
<tr ndTableRow>
<th ndTableHead scope="row">{{ invoice.id }}</th>
<td ndTableCell>{{ invoice.status }}</td>
<td ndTableCell>{{ invoice.method }}</td>
<td ndTableCell class="text-end">{{ invoice.amount }}</td>
</tr>
}
</tbody>
<tfoot ndTableFooter>
<tr ndTableRow>
<th ndTableHead scope="row" colspan="3">Total</th>
<td ndTableCell class="text-end">$1,750.00</td>
</tr>
</tfoot>
</table>
</div>
`,
})
export class TableDemoExample {
protected readonly invoices = [
{ id: 'INV001', status: 'Paid', method: 'Credit card', amount: '$250.00' },
{ id: 'INV002', status: 'Pending', method: 'PayPal', amount: '$150.00' },
{
id: 'INV003',
status: 'Unpaid',
method: 'Bank transfer',
amount: '$350.00',
},
{ id: 'INV004', status: 'Paid', method: 'Credit card', amount: '$450.00' },
{ id: 'INV005', status: 'Paid', method: 'PayPal', amount: '$550.00' },
];
}
Table styles a native HTML table: its caption, header, body, footer, rows
and cells. Because each part is the table's own element, screen readers
announce rows, columns and headers as they should. Wrap it in
ndTableContainer so a wide table scrolls sideways instead of the page.
Installation
npx @ndop/cli add tablepnpm dlx @ndop/cli add tableyarn dlx @ndop/cli add tablebunx @ndop/cli add tableInstall the dependencies
npm install @ndop/corepnpm add @ndop/coreyarn add @ndop/corebun add @ndop/coreCopy the code into your project
At the paths your
ndop.jsonaliases point to; these use the defaults.src/app/ui/table/table.tsimport { computed, Directive, input } from '@angular/core'; import { cn } from '@/lib/utils'; /** * Scrolls a wide table sideways instead of the page. Wrap the * `table[ndTable]` in it. * * @example * ```html * <div ndTableContainer><table ndTable>…</table></div> * ``` */ @Directive({ selector: '[ndTableContainer]', host: { 'data-slot': 'table-container', '[class]': 'classes()' }, }) export class TableContainer { /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn('relative w-full overflow-x-auto', this.class()), ); } /** * A styled native table. Its parts style the table's own elements, so the * table keeps all of its semantics. * * @example * ```html * <table ndTable> * <caption ndTableCaption>Recent invoices</caption> * <thead ndTableHeader><tr ndTableRow><th ndTableHead>Invoice</th></tr></thead> * <tbody ndTableBody><tr ndTableRow><td ndTableCell>INV001</td></tr></tbody> * </table> * ``` */ @Directive({ selector: 'table[ndTable]', host: { 'data-slot': 'table', '[class]': 'classes()' }, }) export class Table { /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn('w-full caption-bottom text-sm', this.class()), ); } /** * A table's header rows. * * @example * ```html * <thead ndTableHeader>…</thead> * ``` */ @Directive({ selector: 'thead[ndTableHeader]', host: { 'data-slot': 'table-header', '[class]': 'classes()' }, }) export class TableHeader { /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn('[&_tr]:border-b', this.class()), ); } /** * A table's body rows. * * @example * ```html * <tbody ndTableBody>…</tbody> * ``` */ @Directive({ selector: 'tbody[ndTableBody]', host: { 'data-slot': 'table-body', '[class]': 'classes()' }, }) export class TableBody { /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn('[&_tr:last-child]:border-0', this.class()), ); } /** * A table's footer rows, such as totals. * * @example * ```html * <tfoot ndTableFooter>…</tfoot> * ``` */ @Directive({ selector: 'tfoot[ndTableFooter]', host: { 'data-slot': 'table-footer', '[class]': 'classes()' }, }) export class TableFooter { /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn('border-t bg-muted/50 font-medium [&>tr]:last:border-b-0', this.class()), ); } /** * A table row; `data-state="selected"` highlights it. * * @example * ```html * <tr ndTableRow>…</tr> * ``` */ @Directive({ selector: 'tr[ndTableRow]', host: { 'data-slot': 'table-row', '[class]': 'classes()' }, }) export class TableRow { /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn( 'border-b transition-colors hover:bg-muted/50 has-aria-expanded:bg-muted/50 data-[state=selected]:bg-muted', this.class(), ), ); } /** * A header cell. Add `scope="col"` or `scope="row"` as fits. * * @example * ```html * <th ndTableHead scope="col">Status</th> * ``` */ @Directive({ selector: 'th[ndTableHead]', host: { 'data-slot': 'table-head', '[class]': 'classes()' }, }) export class TableHead { /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn( 'h-10 px-2 text-start align-middle font-medium whitespace-nowrap text-foreground [&:has([role=checkbox])]:pe-0 [&>[role=checkbox]]:translate-y-[2px]', this.class(), ), ); } /** * A data cell. * * @example * ```html * <td ndTableCell>INV001</td> * ``` */ @Directive({ selector: 'td[ndTableCell]', host: { 'data-slot': 'table-cell', '[class]': 'classes()' }, }) export class TableCell { /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn( 'p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pe-0 [&>[role=checkbox]]:translate-y-[2px]', this.class(), ), ); } /** * A table's caption, which names it; shown under the table. * * @example * ```html * <caption ndTableCaption>Recent invoices</caption> * ``` */ @Directive({ selector: 'caption[ndTableCaption]', host: { 'data-slot': 'table-caption', '[class]': 'classes()' }, }) export class TableCaption { /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn('mt-4 text-sm text-muted-foreground', this.class()), ); }src/app/lib/utils.ts/** * Class helpers used by every styled item. They come from `@ndop/core`; * this file is yours, so it is the place to customize them, for example by * wrapping `cn` with an extended tailwind-merge configuration for your own * utilities. * * @packageDocumentation */ export { cn, variants, type ClassValue, type VariantOf } from '@ndop/core';Update the import paths to match your project
The files import each other through the
@/uiand@/libaliases.
Usage
import {
Table,
TableBody,
TableCaption,
TableCell,
TableContainer,
TableFooter,
TableHead,
TableHeader,
TableRow,
} from '@/ui/table/table';<div ndTableContainer>
<table ndTable>
<caption ndTableCaption>
A list of your recent invoices.
</caption>
<thead ndTableHeader>
<tr ndTableRow>
<th ndTableHead scope="col" class="w-24">Invoice</th>
<th ndTableHead scope="col">Status</th>
<th ndTableHead scope="col">Method</th>
<th ndTableHead scope="col" class="text-end">Amount</th>
</tr>
</thead>
<tbody ndTableBody>
@for (invoice of invoices; track invoice.id) {
<tr ndTableRow>
<th ndTableHead scope="row">{{ invoice.id }}</th>
<td ndTableCell>{{ invoice.status }}</td>
<td ndTableCell>{{ invoice.method }}</td>
<td ndTableCell class="text-end">{{ invoice.amount }}</td>
</tr>
}
</tbody>
<tfoot ndTableFooter>
<tr ndTableRow>
<th ndTableHead scope="row" colspan="3">Total</th>
<td ndTableCell class="text-end">$1,750.00</td>
</tr>
</tfoot>
</table>
</div>Examples
Table status
Orders with a status and an action on each row.
| Order | Customer | Status | Actions |
|---|---|---|---|
| #3210 | Olivia Martin | Delivered | |
| #3209 | Jackson Lee | Shipped | |
| #3204 | Isabella Nguyen | Cancelled |
import { Component } from '@angular/core';
import { Badge, type BadgeVariant } from '@/ui/badge/badge';
import { Button } from '@/ui/button/button';
import {
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableHeader,
TableRow,
} from '@/ui/table/table';
/** One order's status. */
type Status = 'Delivered' | 'Shipped' | 'Cancelled';
/**
* Orders with a status badge and an action on each row; each button's
* name says which order it acts on.
*/
@Component({
selector: 'example-table-status',
imports: [
Badge,
Button,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableHeader,
TableRow,
],
template: `
<div ndTableContainer class="rounded-md border">
<table ndTable aria-label="Orders">
<thead ndTableHeader>
<tr ndTableRow>
<th ndTableHead scope="col">Order</th>
<th ndTableHead scope="col">Customer</th>
<th ndTableHead scope="col">Status</th>
<th ndTableHead scope="col">
<span class="sr-only">Actions</span>
</th>
</tr>
</thead>
<tbody ndTableBody>
@for (order of orders; track order.id) {
<tr ndTableRow>
<th ndTableHead scope="row">{{ order.id }}</th>
<td ndTableCell>{{ order.customer }}</td>
<td ndTableCell>
<span ndBadge [variant]="variants[order.status]">{{
order.status
}}</span>
</td>
<td ndTableCell class="text-end">
<button
ndButton
variant="ghost"
size="sm"
[attr.aria-label]="'View order ' + order.id"
>
View
</button>
</td>
</tr>
}
</tbody>
</table>
</div>
`,
})
export class TableStatusExample {
protected readonly variants: Record<Status, BadgeVariant> = {
Delivered: 'secondary',
Shipped: 'outline',
Cancelled: 'destructive',
};
protected readonly orders: {
id: string;
customer: string;
status: Status;
}[] = [
{ id: '#3210', customer: 'Olivia Martin', status: 'Delivered' },
{ id: '#3209', customer: 'Jackson Lee', status: 'Shipped' },
{ id: '#3204', customer: 'Isabella Nguyen', status: 'Cancelled' },
];
}
Accessibility
Table has no keyboard interaction or focus handling of its own; it keeps the semantics of the element it's on.
API reference
TableContainer
[ndTableContainer]Scrolls a wide table sideways instead of the page. Wrap the
table[ndTable] in it.
| TableContainer binding | Type | Default |
|---|---|---|
[class]Extra classes, merged last so they win. | string | '' |
| Data attribute | Values |
|---|---|
data-slot | table-container |
Table
table[ndTable]A styled native table. Its parts style the table's own elements, so the table keeps all of its semantics.
| Table binding | Type | Default |
|---|---|---|
[class]Extra classes, merged last so they win. | string | '' |
| Data attribute | Values |
|---|---|
data-slot | table |
TableHeader
thead[ndTableHeader]A table's header rows.
| TableHeader binding | Type | Default |
|---|---|---|
[class]Extra classes, merged last so they win. | string | '' |
| Data attribute | Values |
|---|---|
data-slot | table-header |
TableBody
tbody[ndTableBody]A table's body rows.
| TableBody binding | Type | Default |
|---|---|---|
[class]Extra classes, merged last so they win. | string | '' |
| Data attribute | Values |
|---|---|
data-slot | table-body |
TableFooter
tfoot[ndTableFooter]A table's footer rows, such as totals.
| TableFooter binding | Type | Default |
|---|---|---|
[class]Extra classes, merged last so they win. | string | '' |
| Data attribute | Values |
|---|---|
data-slot | table-footer |
TableRow
tr[ndTableRow]A table row; data-state="selected" highlights it.
| TableRow binding | Type | Default |
|---|---|---|
[class]Extra classes, merged last so they win. | string | '' |
| Data attribute | Values |
|---|---|
data-slot | table-row |
TableHead
th[ndTableHead]A header cell. Add scope="col" or scope="row" as fits.
| TableHead binding | Type | Default |
|---|---|---|
[class]Extra classes, merged last so they win. | string | '' |
| Data attribute | Values |
|---|---|
data-slot | table-head |
TableCell
td[ndTableCell]A data cell.
| TableCell binding | Type | Default |
|---|---|---|
[class]Extra classes, merged last so they win. | string | '' |
| Data attribute | Values |
|---|---|
data-slot | table-cell |
TableCaption
caption[ndTableCaption]A table's caption, which names it; shown under the table.
| TableCaption binding | Type | Default |
|---|---|---|
[class]Extra classes, merged last so they win. | string | '' |
| Data attribute | Values |
|---|---|
data-slot | table-caption |
Agent notes
Do
- Name the table with
caption[ndTableCaption], oraria-labelwhen a visible caption doesn't fit the design. - Give header cells a
scope:colfor column headers,rowfor row headers. - Use Data Table for sorting, filtering, selection and pagination.
Don't
- Don't use a table for layout; use flex or grid.
- Don't put interactive controls in every cell without labels that say which row they act on.