Skip to main content

Table

Organize dense data. Clean, readable rows. Facilitate analysis.

Billing history for the Acme workspace.
InvoiceBilling periodStatusAmount
INV-2481Mar 2025Paid$248.00
INV-2480Feb 2025Paid$248.00
INV-2479Jan 2025Pending$248.00
INV-2478Dec 2024Overdue$186.00
Billed this period$930.00
1import { Badge } from '@/components/ui/badge';
2import { Table } from '@/components/ui/table';
3
4const invoices = [
5 {
6 id: 'INV-2481',
7 period: 'Mar 2025',
8 status: 'Paid',
9 tone: 'success',
10 amount: '$248.00',
11 },
12 {
13 id: 'INV-2480',
14 period: 'Feb 2025',
15 status: 'Paid',
16 tone: 'success',
17 amount: '$248.00',
18 },
19 {
20 id: 'INV-2479',
21 period: 'Jan 2025',
22 status: 'Pending',
23 tone: 'warning',
24 amount: '$248.00',
25 },
26 {
27 id: 'INV-2478',
28 period: 'Dec 2024',
29 status: 'Overdue',
30 tone: 'destructive',
31 amount: '$186.00',
32 },
33] as const;
34
35export function Default() {
36 return (
37 <Table>
38 <Table.Caption>Billing history for the Acme workspace.</Table.Caption>
39 <Table.Header>
40 <Table.Row>
41 <Table.Head className="w-[110px]">Invoice</Table.Head>
42 <Table.Head>Billing period</Table.Head>
43 <Table.Head>Status</Table.Head>
44 <Table.Head className="text-right">Amount</Table.Head>
45 </Table.Row>
46 </Table.Header>
47 <Table.Body>
48 {invoices.map((invoice) => (
49 <Table.Row key={invoice.id}>
50 <Table.Cell className="font-medium">{invoice.id}</Table.Cell>
51 <Table.Cell>{invoice.period}</Table.Cell>
52 <Table.Cell>
53 <Badge variant={invoice.tone}>{invoice.status}</Badge>
54 </Table.Cell>
55 <Table.Cell className="text-right tabular-nums">{invoice.amount}</Table.Cell>
56 </Table.Row>
57 ))}
58 </Table.Body>
59 <Table.Footer>
60 <Table.Row>
61 <Table.Cell colSpan={3}>Billed this period</Table.Cell>
62 <Table.Cell className="text-right tabular-nums">$930.00</Table.Cell>
63 </Table.Row>
64 </Table.Footer>
65 </Table>
66 );
67}

Installation

pnpm dlx nachui add table

Anatomy

1import { Table } from '@/components/ui/table';
1<Table>
2 <Table.Caption>A list of your recent invoices.</Table.Caption>
3 <Table.Header>
4 <Table.Row>
5 <Table.Head>Head</Table.Head>
6 </Table.Row>
7 </Table.Header>
8 <Table.Body>
9 <Table.Row>
10 <Table.Cell>Cell</Table.Cell>
11 </Table.Row>
12 </Table.Body>
13 <Table.Footer>
14 <Table.Row>
15 <Table.Cell>Footer</Table.Cell>
16 </Table.Row>
17 </Table.Footer>
18</Table>

Composition

Use the following composition to build a Table:
Table
├── Table.Caption
├── Table.Header
│ └── Table.Row
│ └── Table.Head
├── Table.Body
│ └── Table.Row
│ └── Table.Cell
└── Table.Footer
└── Table.Row
└── Table.Cell

Variants

With Actions

API keys for the storefront-api project.
NameKeyStatusActions
Productionsk_live_••••4f2aActiveUsed 2m ago
CI pipelinesk_live_••••9b71ActiveUsed 3h ago
Local devsk_test_••••1c05ActiveUsed Mar 12
1'use client';
2
3import { useState } from 'react';
4
5import { Badge } from '@/components/ui/badge';
6import { Button } from '@/components/ui/button';
7import { Table } from '@/components/ui/table';
8
9const apiKeys = [
10 { id: 'key_prod', name: 'Production', secret: 'sk_live_••••4f2a', lastUsed: '2m ago' },
11 { id: 'key_ci', name: 'CI pipeline', secret: 'sk_live_••••9b71', lastUsed: '3h ago' },
12 { id: 'key_dev', name: 'Local dev', secret: 'sk_test_••••1c05', lastUsed: 'Mar 12' },
13];
14
15export function WithActions() {
16 const [revoked, setRevoked] = useState<string[]>([]);
17
18 return (
19 <Table>
20 <Table.Caption>API keys for the storefront-api project.</Table.Caption>
21 <Table.Header>
22 <Table.Row>
23 <Table.Head>Name</Table.Head>
24 <Table.Head>Key</Table.Head>
25 <Table.Head>Status</Table.Head>
26 <Table.Head className="text-right">Actions</Table.Head>
27 </Table.Row>
28 </Table.Header>
29 <Table.Body>
30 {apiKeys.map((apiKey) => {
31 const isRevoked = revoked.includes(apiKey.id);
32
33 return (
34 <Table.Row key={apiKey.id}>
35 <Table.Cell className="font-medium">{apiKey.name}</Table.Cell>
36 <Table.Cell className="font-mono">{apiKey.secret}</Table.Cell>
37 <Table.Cell>
38 {isRevoked ? (
39 <Badge variant="secondary">Revoked</Badge>
40 ) : (
41 <span className="flex items-center gap-2">
42 <Badge variant="success">Active</Badge>
43 <span className="text-muted-foreground">Used {apiKey.lastUsed}</span>
44 </span>
45 )}
46 </Table.Cell>
47 <Table.Cell className="text-right">
48 <div className="flex justify-end gap-2">
49 <Button variant="ghost" className="h-8 px-2 text-xs" disabled={isRevoked}>
50 Copy
51 </Button>
52 <Button
53 variant="ghost"
54 className="text-destructive-text h-8 px-2 text-xs"
55 disabled={isRevoked}
56 onClick={() => setRevoked((keys) => [...keys, apiKey.id])}
57 >
58 Revoke
59 </Button>
60 </div>
61 </Table.Cell>
62 </Table.Row>
63 );
64 })}
65 </Table.Body>
66 </Table>
67 );
68}

Striped

Traffic by endpoint over the last 24 hours.
EndpointRequestsError rateStatus
GET /v1/orders128,4020.02%Healthy
POST /v1/orders41,9080.11%Healthy
GET /v1/customers96,1200.04%Healthy
POST /v1/webhooks/stripe8,4412.40%Degraded
DELETE /v1/sessions3,2045.80%Failing
1import { Badge } from '@/components/ui/badge';
2import { Table } from '@/components/ui/table';
3import { cn } from '@/lib/cn';
4
5const endpoints = [
6 {
7 route: 'GET /v1/orders',
8 requests: '128,402',
9 errorRate: '0.02%',
10 status: 'Healthy',
11 tone: 'success',
12 },
13 {
14 route: 'POST /v1/orders',
15 requests: '41,908',
16 errorRate: '0.11%',
17 status: 'Healthy',
18 tone: 'success',
19 },
20 {
21 route: 'GET /v1/customers',
22 requests: '96,120',
23 errorRate: '0.04%',
24 status: 'Healthy',
25 tone: 'success',
26 },
27 {
28 route: 'POST /v1/webhooks/stripe',
29 requests: '8,441',
30 errorRate: '2.40%',
31 status: 'Degraded',
32 tone: 'warning',
33 },
34 {
35 route: 'DELETE /v1/sessions',
36 requests: '3,204',
37 errorRate: '5.80%',
38 status: 'Failing',
39 tone: 'destructive',
40 },
41] as const;
42
43export function Striped() {
44 return (
45 <Table>
46 <Table.Caption>Traffic by endpoint over the last 24 hours.</Table.Caption>
47 <Table.Header>
48 <Table.Row>
49 <Table.Head>Endpoint</Table.Head>
50 <Table.Head className="text-right">Requests</Table.Head>
51 <Table.Head className="text-right">Error rate</Table.Head>
52 <Table.Head>Status</Table.Head>
53 </Table.Row>
54 </Table.Header>
55 <Table.Body>
56 {endpoints.map((endpoint, index) => (
57 <Table.Row key={endpoint.route} className={cn(index % 2 === 0 && 'bg-muted/30')}>
58 <Table.Cell className="font-mono font-medium">{endpoint.route}</Table.Cell>
59 <Table.Cell className="text-right tabular-nums">{endpoint.requests}</Table.Cell>
60 <Table.Cell className="text-right tabular-nums">{endpoint.errorRate}</Table.Cell>
61 <Table.Cell>
62 <Badge variant={endpoint.tone}>{endpoint.status}</Badge>
63 </Table.Cell>
64 </Table.Row>
65 ))}
66 </Table.Body>
67 </Table>
68 );
69}

Compact

DeploymentBranchStatusDuration
dpl_9fk2axmainReady42s
dpl_7ta1mvfeat/checkout-v2Building1m 12s
dpl_5qr8bdfix/webhook-retryFailed18s
dpl_3nz6ykmainReady39s
dpl_2hb4lpchore/bump-depsCanceled6s
1import { Badge } from '@/components/ui/badge';
2import { Table } from '@/components/ui/table';
3
4const deployments = [
5 {
6 id: 'dpl_9fk2ax',
7 branch: 'main',
8 status: 'Ready',
9 tone: 'success',
10 duration: '42s',
11 },
12 {
13 id: 'dpl_7ta1mv',
14 branch: 'feat/checkout-v2',
15 status: 'Building',
16 tone: 'info',
17 duration: '1m 12s',
18 },
19 {
20 id: 'dpl_5qr8bd',
21 branch: 'fix/webhook-retry',
22 status: 'Failed',
23 tone: 'destructive',
24 duration: '18s',
25 },
26 {
27 id: 'dpl_3nz6yk',
28 branch: 'main',
29 status: 'Ready',
30 tone: 'success',
31 duration: '39s',
32 },
33 {
34 id: 'dpl_2hb4lp',
35 branch: 'chore/bump-deps',
36 status: 'Canceled',
37 tone: 'secondary',
38 duration: '6s',
39 },
40] as const;
41
42export function Compact() {
43 return (
44 <Table>
45 <Table.Header>
46 <Table.Row>
47 <Table.Head className="h-auto py-2 text-xs">Deployment</Table.Head>
48 <Table.Head className="h-auto py-2 text-xs">Branch</Table.Head>
49 <Table.Head className="h-auto py-2 text-xs">Status</Table.Head>
50 <Table.Head className="h-auto py-2 text-right text-xs">Duration</Table.Head>
51 </Table.Row>
52 </Table.Header>
53 <Table.Body>
54 {deployments.map((deployment) => (
55 <Table.Row key={deployment.id}>
56 <Table.Cell className="py-2 font-mono font-medium">{deployment.id}</Table.Cell>
57 <Table.Cell className="text-muted-foreground py-2">{deployment.branch}</Table.Cell>
58 <Table.Cell className="py-2">
59 <Badge variant={deployment.tone}>{deployment.status}</Badge>
60 </Table.Cell>
61 <Table.Cell className="py-2 text-right tabular-nums">{deployment.duration}</Table.Cell>
62 </Table.Row>
63 ))}
64 </Table.Body>
65 </Table>
66 );
67}

API Reference

Table

PropTypeDefaultDescription
classNamestringAdditional CSS classes

Table.Header

PropTypeDefaultDescription
classNamestringAdditional CSS classes

Table.Body

PropTypeDefaultDescription
classNamestringAdditional CSS classes

Table.Footer

PropTypeDefaultDescription
classNamestringAdditional CSS classes

Table.Row

PropTypeDefaultDescription
classNamestringAdditional CSS classes

Table.Head

PropTypeDefaultDescription
classNamestringAdditional CSS classes

Table.Cell

PropTypeDefaultDescription
classNamestringAdditional CSS classes

Table.Caption

PropTypeDefaultDescription
classNamestringAdditional CSS classes
Found something to improve?

Notice a bug, typo, or missing detail on this page? Help us make the documentation better by opening a GitHub issue.

Create an Issue
Ctrl+I