Styling Cells, Headers, and Rows
The visible parts of the table — cells, headers, the outer frame — all accept Tailwind classes the way any component in your app does. Reach for column-level styling when a single column needs to look different (a status pill, a right-aligned number, a muted secondary line) and wrapper-level styling when the change applies to the whole table.
Column-level styling
Section titled “Column-level styling”import { DataTable, DataTableCell, DataTableColumn, DataTableColumnHeader } from '@/components/ui/data-table'
import { localModel } from '@virtuoso.dev/data-table'
const rows = [
{ name: 'Standing Desk', status: 'Launch ready', theme: 'Office' },
{ name: 'USB-C Dock', status: 'Low stock', theme: 'Peripherals' },
{ name: 'Noise-canceling Headset', status: 'Backorder', theme: 'Audio' },
]
const model = localModel({ data: rows })
export default function App() {
return (
<DataTable model={model} style={{ height: 280 }}>
<DataTableColumn field="name">
<DataTableColumnHeader>Product</DataTableColumnHeader>
<DataTableCell className="font-medium">{({ cellValue }) => String(cellValue)}</DataTableCell>
</DataTableColumn>
<DataTableColumn field="theme">
<DataTableColumnHeader>Theme</DataTableColumnHeader>
<DataTableCell>
{({ cellValue }) => <span className="rounded-full bg-slate-100 px-2 py-1 text-xs font-medium">{String(cellValue)}</span>}
</DataTableCell>
</DataTableColumn>
<DataTableColumn field="status">
<DataTableColumnHeader>Status</DataTableColumnHeader>
<DataTableCell>
{({ cellValue }) => (
<span
className={
cellValue === 'Backorder'
? 'font-medium text-red-600'
: cellValue === 'Low stock'
? 'font-medium text-amber-600'
: 'font-medium text-emerald-600'
}
>
{String(cellValue)}
</span>
)}
</DataTableCell>
</DataTableColumn>
</DataTable>
)
}className on DataTableCell styles the positioned cell. A column header has two widths, so it exposes two styling surfaces:
DataTableColumnHeader.classNamestyles the intrinsic header content and sets the column’s base width.DataTableColumnHeader.containerPropsstyles the full rendered column width and accepts event handlers and ARIA attributes.
The table measures the intrinsic surface before it distributes spare width through grow. Measuring the rendered surface instead would feed the grown width back in as the next base width, so the column could not shrink with its viewport. containerProps provides a separate full-width paint and interaction surface without changing that measurement.
Use containerProps when a background, hover state, outline, box shadow, cursor, or click target must cover the spare width:
<DataTableColumn field="name" grow={1}>
<DataTableColumnHeader className="min-w-64" containerProps={{ className: 'cursor-pointer hover:bg-muted/50' }}>
Product
</DataTableColumnHeader>
<DataTableCell className="font-medium">{({ cellValue }) => String(cellValue)}</DataTableCell>
</DataTableColumn>Keep layout on className. The table owns the outer container’s geometry, so width, flex, margin, padding, border, display, and justify-* styles on containerProps can clip content or move edge-aligned slots. Use box-shadow or outline instead of a border on that surface, and use HeaderEnd for controls that must align to the rendered edge.
Container events receive bubbled events from nested controls and header slots. If the header also contains sorting, resizing, reordering, links, or buttons, stop propagation in those controls or guard the container handler before running the whole-header action.
Render-function children still let you wrap only the value when it needs its own markup, such as a badge or two-line label.
Wrapper-level styling
Section titled “Wrapper-level styling”className on <DataTable> reaches the outer frame around the scroll viewport. The shadcn wrapper already provides the default frame (rounded-md border) and table surface, so you do not need to repeat rounded-md border at each instance. Use wrapper classes for intentional frame overrides, shadows, and table-level CSS variable overrides.
<DataTable className="rounded-xl shadow-sm" model={model}>
{/* columns */}
</DataTable>To remove or strengthen the default frame, override it directly:
<DataTable className="border-0" model={model}>
{/* columns */}
</DataTable>
<DataTable className="border-2" model={model}>
{/* columns */}
</DataTable>For pixel sizing, prefer style (style={{ height: 360 }}) — the table needs a measurable height to virtualize, and Tailwind’s h-* utilities work too, as long as the resolved height is real (not a percentage of an unmeasured parent).
What this page doesn’t cover
Section titled “What this page doesn’t cover”Two adjacent concerns have their own pages:
- Replacing the table’s structural components (Row, GroupHeader, sticky wrappers, scroll element) lives in Replacing Internals. That’s the
componentsprop pattern. - Default classes baked into the shadcn wrapper — change those by editing
@/components/ui/data-tabledirectly. See The shadcn Wrapper for the wrapper anatomy.