Table composition primitives
| 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 |
| INV006 | Pending | Bank Transfer | $200.00 |
| INV007 | Unpaid | Credit Card | $300.00 |
| Total | $2,500.00 | ||
import { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow } from '@/components/ui/table'
...import { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow } from '@/components/ui/table'
const invoices = [
['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'],
['INV006', 'Pending', 'Bank Transfer', '$200.00'],
['INV007', 'Unpaid', 'Credit Card', '$300.00'],
] as const
export default function TableDemoExample() {
return (
<Table>
<TableCaption>A list of your recent invoices.</TableCaption>
<TableHeader><TableRow><TableHead class="w-[100px]">Invoice</TableHead><TableHead>Status</TableHead><TableHead>Method</TableHead><TableHead class="text-right">Amount</TableHead></TableRow></TableHeader>
<TableBody>{invoices.map(invoice => <TableRow><TableCell class="font-medium">{invoice[0]}</TableCell><TableCell>{invoice[1]}</TableCell><TableCell>{invoice[2]}</TableCell><TableCell class="text-right">{invoice[3]}</TableCell></TableRow>)}</TableBody>
<TableFooter><TableRow><TableCell colSpan={3}>Total</TableCell><TableCell class="text-right">$2,500.00</TableCell></TableRow></TableFooter>
</Table>
)
}
Installation
npx @fictjs/shadcn@latest add tableCopy the Fict shadcn component into your project.
Source
components/ui/table.tsx
src/components/ui/table.tsx
import { cn } from '@/lib/cn'
type DivProps = JSX.IntrinsicElements['div']
type TableProps = JSX.IntrinsicElements['table']
type SectionProps = JSX.IntrinsicElements['thead']
type BodyProps = JSX.IntrinsicElements['tbody']
type RowProps = JSX.IntrinsicElements['tr']
type HeadProps = JSX.IntrinsicElements['th']
type CellProps = JSX.IntrinsicElements['td']
type CaptionProps = JSX.IntrinsicElements['caption']
export function Table(props: TableProps) {
const { class: className, ...rest } = props
return (
<div class='relative w-full overflow-auto'>
<table class={cn('w-full caption-bottom text-sm', className)} {...rest} />
</div>
)
}
export function TableHeader(props: SectionProps) {
const { class: className, ...rest } = props
return <thead class={cn('[&_tr]:border-b', className)} {...rest} />
}
export function TableBody(props: BodyProps) {
const { class: className, ...rest } = props
return <tbody class={cn('[&_tr:last-child]:border-0', className)} {...rest} />
}
export function TableFooter(props: SectionProps) {
const { class: className, ...rest } = props
return <tfoot class={cn('border-t bg-muted/50 font-medium [&>tr]:last:border-b-0', className)} {...rest} />
}
export function TableRow(props: RowProps) {
const { class: className, ...rest } = props
return <tr class={cn('border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted', className)} {...rest} />
}
export function TableHead(props: HeadProps) {
const { class: className, ...rest } = props
return <th class={cn('h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0', className)} {...rest} />
}
export function TableCell(props: CellProps) {
const { class: className, ...rest } = props
return <td class={cn('p-2 align-middle [&:has([role=checkbox])]:pr-0', className)} {...rest} />
}
export function TableCaption(props: CaptionProps) {
const { class: className, ...rest } = props
return <caption class={cn('mt-4 text-sm text-muted-foreground', className)} {...rest} />
}
export function TableContainer(props: DivProps) {
const { class: className, ...rest } = props
return <div class={cn('w-full rounded-md border', className)} {...rest} />
}
Usage
Examples
Footer
| Invoice | Status | Method | Amount |
|---|---|---|---|
| INV001 | Paid | Credit Card | $250.00 |
| INV002 | Pending | PayPal | $150.00 |
| INV003 | Unpaid | Bank Transfer | $350.00 |
| Total | $2,500.00 | ||
import { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow } from '@/components/ui/table'
...import { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow } from '@/components/ui/table'
const invoices = [
['INV001', 'Paid', 'Credit Card', '$250.00'],
['INV002', 'Pending', 'PayPal', '$150.00'],
['INV003', 'Unpaid', 'Bank Transfer', '$350.00'],
] as const
export default function TableFooterExample() {
return (
<Table>
<TableCaption>A list of your recent invoices.</TableCaption>
<TableHeader><TableRow><TableHead class="w-[100px]">Invoice</TableHead><TableHead>Status</TableHead><TableHead>Method</TableHead><TableHead class="text-right">Amount</TableHead></TableRow></TableHeader>
<TableBody>{invoices.map(invoice => <TableRow><TableCell class="font-medium">{invoice[0]}</TableCell><TableCell>{invoice[1]}</TableCell><TableCell>{invoice[2]}</TableCell><TableCell class="text-right">{invoice[3]}</TableCell></TableRow>)}</TableBody>
<TableFooter><TableRow><TableCell colSpan={3}>Total</TableCell><TableCell class="text-right">$2,500.00</TableCell></TableRow></TableFooter>
</Table>
)
}
Actions
| Product | Price | Actions |
|---|---|---|
| Wireless Mouse | $29.99 | |
| Mechanical Keyboard | $129.99 | |
| USB-C Hub | $49.99 |
import { Button } from '@/components/ui/button'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
...import { Button } from '@/components/ui/button'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
const products = [['Wireless Mouse', '$29.99'], ['Mechanical Keyboard', '$129.99'], ['USB-C Hub', '$49.99']] as const
function MoreIcon() {
return <svg aria-hidden="true" viewBox="0 0 24 24"><circle cx="5" cy="12" r="1" /><circle cx="12" cy="12" r="1" /><circle cx="19" cy="12" r="1" /></svg>
}
export default function TableActionsExample() {
return (
<Table>
<TableHeader><TableRow><TableHead>Product</TableHead><TableHead>Price</TableHead><TableHead class="text-right">Actions</TableHead></TableRow></TableHeader>
<TableBody>{products.map(([product, price]) => (
<TableRow>
<TableCell class="font-medium">{product}</TableCell><TableCell>{price}</TableCell>
<TableCell class="text-right">
<DropdownMenu>
<DropdownMenuTrigger asChild><Button variant="ghost" size="icon" class="size-8" aria-label={`Open menu for ${product}`}><MoreIcon /></Button></DropdownMenuTrigger>
<DropdownMenuContent align="end"><DropdownMenuItem>Edit</DropdownMenuItem><DropdownMenuItem>Duplicate</DropdownMenuItem><DropdownMenuSeparator /><DropdownMenuItem class="text-destructive">Delete</DropdownMenuItem></DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}</TableBody>
</Table>
)
}
Rtl
| الفاتورة | الحالة | الطريقة | المبلغ |
|---|---|---|---|
| INV001 | مدفوع | بطاقة ائتمانية | $250.00 |
| INV002 | قيد الانتظار | PayPal | $150.00 |
| INV003 | غير مدفوع | تحويل بنكي | $350.00 |
| INV004 | مدفوع | بطاقة ائتمانية | $450.00 |
| INV005 | مدفوع | PayPal | $550.00 |
| INV006 | قيد الانتظار | تحويل بنكي | $200.00 |
| INV007 | غير مدفوع | بطاقة ائتمانية | $300.00 |
| المجموع | $2,500.00 | ||
import { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow } from '@/components/ui/table'
...import { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow } from '@/components/ui/table'
const translations = {
ar: { dir: 'rtl', caption: 'قائمة بفواتيرك الأخيرة.', headings: ['الفاتورة', 'الحالة', 'الطريقة', 'المبلغ'], statuses: ['مدفوع', 'قيد الانتظار', 'غير مدفوع'], methods: ['بطاقة ائتمانية', 'PayPal', 'تحويل بنكي'], total: 'المجموع' },
he: { dir: 'rtl', caption: 'רשימת החשבוניות האחרונות שלך.', headings: ['חשבונית', 'סטטוס', 'שיטה', 'סכום'], statuses: ['שולם', 'ממתין', 'לא שולם'], methods: ['כרטיס אשראי', 'PayPal', 'העברה בנקאית'], total: 'סה״כ' },
en: { dir: 'ltr', caption: 'A list of your recent invoices.', headings: ['Invoice', 'Status', 'Method', 'Amount'], statuses: ['Paid', 'Pending', 'Unpaid'], methods: ['Credit Card', 'PayPal', 'Bank Transfer'], total: 'Total' },
} as const
const invoices = [
['INV001', 0, 0, '$250.00'], ['INV002', 1, 1, '$150.00'], ['INV003', 2, 2, '$350.00'],
['INV004', 0, 0, '$450.00'], ['INV005', 0, 1, '$550.00'], ['INV006', 1, 2, '$200.00'], ['INV007', 2, 0, '$300.00'],
] as const
export default function TableRtlExample() {
let language = $state<keyof typeof translations>('ar')
const t = translations[language]
return (
<div>
<select aria-label="Preview language" value={language} onChange={event => language = event.currentTarget.value as keyof typeof translations}>
<option value="ar">Arabic (العربية)</option><option value="he">Hebrew (עברית)</option><option value="en">English</option>
</select>
<Table dir={t.dir}>
<TableCaption>{t.caption}</TableCaption>
<TableHeader><TableRow>{t.headings.map(heading => <TableHead>{heading}</TableHead>)}</TableRow></TableHeader>
<TableBody>{invoices.map(invoice => <TableRow><TableCell class="font-medium">{invoice[0]}</TableCell><TableCell>{t.statuses[invoice[1]]}</TableCell><TableCell>{t.methods[invoice[2]]}</TableCell><TableCell class="text-right">{invoice[3]}</TableCell></TableRow>)}</TableBody>
<TableFooter><TableRow><TableCell colSpan={3}>{t.total}</TableCell><TableCell class="text-right">$2,500.00</TableCell></TableRow></TableFooter>
</Table>
</div>
)
}