Styling rows and cells
Use this guide when the table needs visual states that ordinary CSS selectors cannot infer on their own.
rowPresentation is the styling seam for translating table-owned interaction state into your design system's classes and data-* attributes.
1. Treat styling as a separate customization layer from rendering
A common mistake is to push every visual need into custom cell renderers.
That works for content, but it is the wrong layer for many styling concerns.
rowPresentation exists so the product can style rows and cells from table-owned state such as:
- selection
- active-row focus
- preview-open state
- whether a row is a data row or a group header
That makes it the right layer for state-aware styling, not content rendering.
2. Let the table own the state and your app own the styling decision
The table already knows whether a row is selected, active, or currently previewed.
rowPresentation lets your application consume that state and decide what classes or DOM attributes should appear.
<Datatable
tableKey="customers"
rowPresentation={{
getRowClassName: ({ isActive, isPreviewOpen }) =>
isActive || isPreviewOpen ? "bg-primary/5" : undefined,
}}
/>This is the important split:
- the table decides what state is true
- your app decides how that state should look
3. Choose the right hook for the surface you want to style
The API exposes separate hooks for rows and cells because those are different visual surfaces.
getRowClassNamestyles the whole row shellgetCellClassNamestyles an individual cellgetRowAttributesadds row-level attributesgetCellAttributesadds cell-level attributes
That lets you keep broad row treatment and precise cell treatment independent.
4. Branch on row kind when grouped tables need different treatment
getRowClassName and getRowAttributes run for both normal data rows and group headers.
That is why the callback receives rowKind.
getRowClassName={(info) => {
if (info.rowKind === "group-header") return "font-medium text-muted-foreground"
return info.isSelected ? "bg-muted" : undefined
}}This is useful because grouped tables often need different styling logic for structural rows versus actual data rows, but they should still participate in one coherent styling system.
5. Use row styling for interaction state, not hidden business meaning
A good use of row styling:
- selected rows get a muted background
- active keyboard row gets a stronger outline
- preview-open rows get a subtle emphasis
- group headers get a distinct structural treatment
A less healthy use:
- encoding important business meaning only through background color
- hiding core status semantics inside classes with no textual cue
- using row hooks to patch over missing data modeling in the column layer
The styling seam should reinforce meaning that already exists, not create the only source of meaning.
6. Use cell styling for local emphasis and per-column polish
getCellClassName is a good fit for targeted treatment such as:
- right-aligning numeric columns
- muting secondary metadata
- emphasizing a risk/status column
- applying stable per-column visual differences
Because it receives columnId, it can express per-column logic without requiring each custom cell to repeat the same styling rules.
getCellClassName={({ columnId }) =>
columnId === "revenue" ? "text-right tabular-nums" : undefined
}7. Prefer attributes for testing and instrumentation seams
getRowAttributes and getCellAttributes support styling, testing, and instrumentation through one stable seam.
getRowAttributes={({ rowKind, rowId }) => ({
"data-row-kind": rowKind,
"data-row-id": rowId,
})}getCellAttributes={({ columnId }) => ({
"data-column-id": columnId,
})}This is usually better than tying tests to visible text that may change frequently.
8. Keep selected, active, and preview-open states visually distinct
These three states can overlap while still carrying different interaction meaning.
- selected means included in selection state
- active means the keyboard/navigation cursor is on this row
- preview-open means the row is driving a preview surface
If all three use identical styling, the table becomes harder to read. Treat them as related but distinct signals.
9. Prefer hooks over ad hoc DOM reach-ins
Because virtualization, grouping, and selection change the rendered structure over time, styling hacks that depend on fragile DOM traversal tend to break.
rowPresentation is the stable surface for this job because it sits close to the table's own row-state model.
10. Use a simple boundary when deciding between styling and rendering
If the change is about what content appears, use a custom cell.
If the change is about how table-owned state should style the row or cell shell, use rowPresentation.
That boundary keeps customization easier to maintain.
Where to go next
- For the broader customization map, read Customization overview.
- For content-level rendering changes, read Custom React cells.
- For the surrounding surface these styles act on, read Table anatomy.