Table
A responsive table component.
| 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 { css } from 'remix/component'
import {
Table,
TableBody,
TableCaption,import { css } from 'remix/component'
import {
Table,
TableBody,
TableCaption,
TableCell,
TableFooter,
TableHead,
TableHeader,
TableRow,
} from '#app/ui/table.tsx'
const invoices = [
{ invoice: 'INV001', paymentStatus: 'Paid', totalAmount: '$250.00', paymentMethod: 'Credit Card' },
{ invoice: 'INV002', paymentStatus: 'Pending', totalAmount: '$150.00', paymentMethod: 'PayPal' },
{
invoice: 'INV003',
paymentStatus: 'Unpaid',
totalAmount: '$350.00',
paymentMethod: 'Bank Transfer',
},
{ invoice: 'INV004', paymentStatus: 'Paid', totalAmount: '$450.00', paymentMethod: 'Credit Card' },
{ invoice: 'INV005', paymentStatus: 'Paid', totalAmount: '$550.00', paymentMethod: 'PayPal' },
{
invoice: 'INV006',
paymentStatus: 'Pending',
totalAmount: '$200.00',
paymentMethod: 'Bank Transfer',
},
{
invoice: 'INV007',
paymentStatus: 'Unpaid',
totalAmount: '$300.00',
paymentMethod: 'Credit Card',
},
]
const textRight = css({ textAlign: 'right' })
export function TableDemo() {
return () => (
<Table>
<TableCaption>A list of your recent invoices.</TableCaption>
<TableHeader>
<TableRow>
<TableHead mix={css({ width: '100px' })}>Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead>Method</TableHead>
<TableHead mix={textRight}>Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{invoices.map((invoice) => (
<TableRow key={invoice.invoice}>
<TableCell mix={css({ fontWeight: 500 })}>{invoice.invoice}</TableCell>
<TableCell>{invoice.paymentStatus}</TableCell>
<TableCell>{invoice.paymentMethod}</TableCell>
<TableCell mix={textRight}>{invoice.totalAmount}</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TableCell colSpan={3}>Total</TableCell>
<TableCell mix={textRight}>$2,500.00</TableCell>
</TableRow>
</TableFooter>
</Table>
)
}Installation
$ bunx --bun shadcn@latest add https://www.shadcn-remix.com/r/table.json$ npx shadcn@latest add https://www.shadcn-remix.com/r/table.json$ pnpm dlx shadcn@latest add https://www.shadcn-remix.com/r/table.json$ yarn dlx shadcn@latest add https://www.shadcn-remix.com/r/table.jsonInstall the following dependencies:
$ bun add remix$ npm install remix$ pnpm add remix$ yarn add remixCopy and paste the following code into your project.
import { css, type Handle, type Props } from 'remix/component'
const checkboxCell = {
'&:has([role="checkbox"])': { paddingRight: 0 },
'& > [role="checkbox"]': { translate: '0 2px' },
}
const containerStyles = css({ position: 'relative', width: '100%', overflowX: 'auto' })
const tableStyles = css({
width: '100%',
captionSide: 'bottom',
fontSize: '0.875rem',
lineHeight: 'calc(1.25 / 0.875)',
})
const tableHeaderStyles = css({ '& tr': { borderBottomWidth: '1px' } })
const tableBodyStyles = css({ '& tr:last-child': { borderWidth: 0 } })
const tableFooterStyles = css({
borderTopWidth: '1px',
backgroundColor: 'color-mix(in oklab, var(--muted) 50%, transparent)',
fontWeight: 500,
'& > tr:last-child': { borderBottomWidth: 0 },
})
const tableRowStyles = css({
borderBottomWidth: '1px',
transitionProperty:
'color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to',
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
transitionDuration: '150ms',
'&:hover': { backgroundColor: 'color-mix(in oklab, var(--muted) 50%, transparent)' },
'&:has([aria-expanded="true"])': {
backgroundColor: 'color-mix(in oklab, var(--muted) 50%, transparent)',
},
'&[data-state="selected"]': { backgroundColor: 'var(--muted)' },
// TableBody/TableFooter remove the last row's border. Repeated here because this descriptor's
// cascade layer may come after theirs, where its own border would otherwise win.
'[data-slot="table-body"] &:last-child': { borderWidth: 0 },
'[data-slot="table-footer"] > &:last-child': { borderBottomWidth: 0 },
})
const tableHeadStyles = css({
height: '2.5rem',
paddingInline: '0.5rem',
textAlign: 'left',
verticalAlign: 'middle',
fontWeight: 500,
whiteSpace: 'nowrap',
color: 'var(--foreground)',
...checkboxCell,
})
const tableCellStyles = css({
padding: '0.5rem',
verticalAlign: 'middle',
whiteSpace: 'nowrap',
...checkboxCell,
})
const tableCaptionStyles = css({
marginTop: '1rem',
fontSize: '0.875rem',
lineHeight: 'calc(1.25 / 0.875)',
color: 'var(--muted-foreground)',
})
export function Table(handle: Handle<Props<'table'>>) {
return () => {
let { mix, ...props } = handle.props
return (
<div data-slot="table-container" mix={containerStyles}>
<table data-slot="table" {...props} mix={[tableStyles, mix]} />
</div>
)
}
}
export function TableHeader(handle: Handle<Props<'thead'>>) {
return () => {
let { mix, ...props } = handle.props
return <thead data-slot="table-header" {...props} mix={[tableHeaderStyles, mix]} />
}
}
export function TableBody(handle: Handle<Props<'tbody'>>) {
return () => {
let { mix, ...props } = handle.props
return <tbody data-slot="table-body" {...props} mix={[tableBodyStyles, mix]} />
}
}
export function TableFooter(handle: Handle<Props<'tfoot'>>) {
return () => {
let { mix, ...props } = handle.props
return <tfoot data-slot="table-footer" {...props} mix={[tableFooterStyles, mix]} />
}
}
export function TableRow(handle: Handle<Props<'tr'>>) {
return () => {
let { mix, ...props } = handle.props
return <tr data-slot="table-row" {...props} mix={[tableRowStyles, mix]} />
}
}
export function TableHead(handle: Handle<Props<'th'>>) {
return () => {
let { mix, ...props } = handle.props
return <th data-slot="table-head" {...props} mix={[tableHeadStyles, mix]} />
}
}
export function TableCell(handle: Handle<Props<'td'>>) {
return () => {
let { mix, ...props } = handle.props
return <td data-slot="table-cell" {...props} mix={[tableCellStyles, mix]} />
}
}
export function TableCaption(handle: Handle<Props<'caption'>>) {
return () => {
let { mix, ...props } = handle.props
return <caption data-slot="table-caption" {...props} mix={[tableCaptionStyles, mix]} />
}
}Update the import paths to match your project setup.
Usage
import {
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '#app/ui/table.tsx'<Table>
<TableCaption>A list of your recent invoices.</TableCaption>
<TableHeader>
<TableRow>
<TableHead mix={css({ width: '100px' })}>Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead>Method</TableHead>
<TableHead mix={css({ textAlign: 'right' })}>Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell mix={css({ fontWeight: 500 })}>INV001</TableCell>
<TableCell>Paid</TableCell>
<TableCell>Credit Card</TableCell>
<TableCell mix={css({ textAlign: 'right' })}>$250.00</TableCell>
</TableRow>
</TableBody>
</Table>Composition
Use the following composition to build a Table:
Table
├── TableCaption
├── TableHeader
│ └── TableRow
│ └── TableHead
├── TableBody
│ └── TableRow
│ └── TableCell
└── TableFooter
└── TableRow
└── TableCellAPI Reference
Table
The <table>, wrapped in a horizontally scrollable container.
| Prop | Type | Default |
|---|---|---|
mix | MixValue | - |
TableHeader
<thead>.
| Prop | Type | Default |
|---|---|---|
mix | MixValue | - |
TableBody
<tbody>.
| Prop | Type | Default |
|---|---|---|
mix | MixValue | - |
TableFooter
<tfoot>.
| Prop | Type | Default |
|---|---|---|
mix | MixValue | - |
TableRow
<tr>. Set data-state="selected" to highlight it.
| Prop | Type | Default |
|---|---|---|
mix | MixValue | - |
TableHead
<th>.
| Prop | Type | Default |
|---|---|---|
mix | MixValue | - |
TableCell
<td>.
| Prop | Type | Default |
|---|---|---|
mix | MixValue | - |
TableCaption
<caption>.
| Prop | Type | Default |
|---|---|---|
mix | MixValue | - |