Tables & Data Grid
Two tiers of tabular UI — nine hand-styled Basic Table variants and an AG Grid Community data grid demoed across six variant routes, themed through design tokens.
Luminaux ships two tabular building blocks so you can pick the lightest tool for the job:
| Page | Route(s) | Source | When to use |
|---|---|---|---|
| Basic Table | /tables/basic | src/pages/tables/BasicTablePage.tsx | Small, read-mostly datasets. Plain HTML <table>s styled with Tailwind tokens, in Panels. No grid lib. |
| Data Grid | /tables/data-grid/* | src/pages/tables/DataGridVariants.tsx + src/components/datagrid/grids.tsx | Larger, interactive datasets needing sorting, filters, selection, editing, pinning, pagination. Built on AG Grid Community. |
Both live under the Data → Tables menu section, where the six grid variants form a nested Data
Grid subgroup. All six grid routes are React.lazy-loaded so AG Grid splits into its own bundle
chunk and never weighs down the initial app load.
AG Grid Community (MIT) only — never the enterprise package
Only ag-grid-community + ag-grid-react (v36, MIT-licensed) are used, registered via
AllCommunityModule. Never install ag-grid-enterprise or enable enterprise-only features (row
grouping panel, pivoting, aggregation, master/detail, server-side row model, Excel export, integrated
charts, set filters). A buyer who needs enterprise features must license it themselves; the template must
not ship it.
The six Data Grid variants
Each variant is its own route (and menu leaf), sharing one dataset (ROWS, 12 seeded users) and one page
shell. Everything used is community-safe — CSV export, quick filter, floating filters, the built-in
agSelectCellEditor/agNumberCellEditor, row pinning, and column pinning are all free features.
| Route | Component (grids.tsx) | What it demos |
|---|---|---|
/tables/data-grid/default | DefaultGrid | Baseline grid: 7 columns, sort/filter/resize on every column, pagination (10 rows, [10, 20, 50]), i18n + currency valueFormatters. |
/tables/data-grid/renderers | RenderersGrid | Custom JSX cell renderers: avatar + name/email cell, Badge status pill, icon action buttons; rowHeight={64}. |
/tables/data-grid/selection | SelectionGrid | Multi-row selection (rowSelection={{mode: 'multiRow'}}), a live selected-count readout, and CSV export. |
/tables/data-grid/search | SearchGrid | A quick-search Input bound to quickFilterText, plus floating filters under every header. |
/tables/data-grid/editable | EditableGrid | Inline editing: editable: true columns with agSelectCellEditor/agNumberCellEditor, on a copied row set. |
/tables/data-grid/pinned | PinnedGrid | Pinned columns (pinned: 'left'/'right') + a pinned bottom total row (pinnedBottomRowData). |
The nine Basic Table variants
BasicTablePage presents each variant in its own Panel (with bodyClassName="p-0 overflow-hidden")
inside a Stagger/StaggerItem cascade: default, striped, bordered, compact, rich
(avatar + email + per-row Dropdown action menu), super-rich (presence Avatar, role pill,
Progress usage bar, spend + TrendPill, star rating, action buttons), selectable (row Checkboxes
- select-all + a selection-count bar), sortable (clickable headers, asc/desc), and paginated
(client-side slice + the
Paginationcomponent).
Architecture & files
| Path | Responsibility |
|---|---|
src/components/datagrid/grids.tsx | The whole grid feature: AllCommunityModule registration, the shared Row dataset, cell renderers, useGridTheme() + useRowStagger() hooks, and the six exported grid components. |
src/pages/tables/DataGridVariants.tsx | Six thin route pages around a shared GridPage shell (PageHeader + icon-chip heading + Card frame). |
src/pages/tables/BasicTablePage.tsx | The nine plain-HTML table variants in Panels. |
src/styles/_datagrid.scss | The .ag-rows-in .ag-row keyframe + :nth-child stagger delays for the grid entrance. |
src/hooks/useChartTokens.ts | Reads the raw --* CSS tokens off <html> so the grid theme re-skins (shared with charts). |
src/data/menu.ts | The Data → Tables section: the nested Data Grid group (6 leaves) + Basic Table. |
Usage
Basic Table
A Basic Table is just JSX in a Panel — semantic-token classes on a plain <table>, a Badge for the
status cell. Put it in an overflow-x-auto div so it scrolls horizontally on narrow screens, and zero out
the Panel body padding so rows run edge-to-edge:
const tone = {active: 'success', pending: 'warning', suspended: 'danger'} as const
const HEAD = 'border-b border-border text-left text-xs uppercase tracking-wider text-muted-foreground'
<Panel bodyClassName="p-0 overflow-hidden" title="Team" subtitle="Read-mostly demo data">
<div className="overflow-x-auto">
<table className="w-full border-collapse text-sm">
<thead><tr className={HEAD}><th className="px-4 py-3 font-semibold">Name</th><th className="px-4 py-3 font-semibold">Status</th></tr></thead>
<tbody>
{rows.map((r) => (
<tr key={r.id} className="border-b border-border transition-colors last:border-0 hover:bg-surface-muted">
<td className="px-4 py-3 text-foreground">{r.name}</td>
<td className="px-4 py-3"><Badge tone={tone[r.status]}>{r.status}</Badge></td>
</tr>
))}
</tbody>
</table>
</div>
</Panel>Data Grid
Register the community module set once at module scope, define columns with ColDef, and theme the
grid from useChartTokens(). The grid must live inside a height-bounded container — AG Grid
virtualizes its rows and needs an explicit height (the variants use GRID_HEIGHT = 540).
import {AgGridReact} from 'ag-grid-react'
import {AllCommunityModule, ModuleRegistry, themeQuartz, type ColDef} from 'ag-grid-community'
// AG Grid Community (free / MIT) — register the community module set once. Never
// add ag-grid-enterprise or enterprise-only features.
ModuleRegistry.registerModules([AllCommunityModule])API / Props
Shared hooks (grids.tsx)
| Hook | Returns | Purpose |
|---|---|---|
useGridTheme() | a memoized Theming-API theme | themeQuartz.withParams({...}) parameterized from useChartTokens(), rebuilt on [tk] so it re-skins. |
useRowStagger() | {ref, onFirstDataRendered} | The one-time entrance: attach ref to the grid wrapper, pass the callback to onFirstDataRendered. Gated by useReducedMotion(). |
AgGridReact props used across the variants
| Prop | Variant(s) | Value / purpose |
|---|---|---|
theme | all | useGridTheme() — Theming API object. |
rowData | all | The shared ROWS seed (Editable uses its own copied array). |
columnDefs | all | ColDef<Row>[] per variant. |
defaultColDef | all | {sortable: true, filter: true, resizable: true} (Search adds floatingFilter: true). |
animateRows | all | AG Grid's built-in animation for sort / filter / column-move. |
onFirstDataRendered | all | Triggers the one-time entrance cascade (useRowStagger). |
pagination + paginationPageSize + paginationPageSizeSelector | Default | Client-side pagination: 10 rows, [10, 20, 50]. |
rowHeight | Renderers | 64 — taller rows for the avatar cells. |
rowSelection | Selection | {mode: 'multiRow'} — the v36 object API (adds the checkbox column). |
quickFilterText | Search | Bound to the controlled Input value. |
pinnedBottomRowData | Pinned | A one-element Row[] carrying the spend total. |
ColDef fields used: field, headerName, flex, maxWidth, valueFormatter, cellRenderer,
editable, cellEditor + cellEditorParams, pinned: 'left' | 'right', and per-column
sortable: false / filter: false. All community-safe.
Custom cell renderers
Renderers receive ICellRendererParams<Row> and return plain JSX. Two conventions worth copying:
- Fill the cell — AG Grid doesn't vertically center custom JSX, so every renderer wraps in
className="flex h-full items-center …". - Guard pinned rows — renderers/formatters on grids with
pinnedBottomRowDatacheckp.node.rowPinned(the actions renderer returnsnullon the total row).
function StatusCell(p: ICellRendererParams<Row>) {
const v = p.value as Status | undefined
if (!v) return null
return <div className="flex h-full items-center"><Badge tone={STATUS_TONE[v]}>{v}</Badge></div>
}Configuration & customization
Theming API (token-driven, so it re-skins)
AG Grid v36 uses the Theming API (JS theme objects), not the legacy CSS theme files. The shared
useGridTheme() starts from themeQuartz and parameterizes it from useChartTokens() so light/dark and
every color skin flow through automatically:
const tk = useChartTokens()
const theme = useMemo(
() =>
themeQuartz.withParams({
accentColor: tk.primary,
backgroundColor: tk.surface,
foregroundColor: tk.foreground,
borderColor: tk.border,
headerBackgroundColor: tk.surfaceMuted,
headerTextColor: tk.mutedForeground,
rowHoverColor: tk.surfaceMuted,
oddRowBackgroundColor: tk.background,
fontFamily: 'inherit',
headerFontWeight: 600,
}),
[tk],
)Because useChartTokens re-reads the raw --* vars whenever <html>'s class (dark), data-skin, or
data-oled attributes change, the theme object is rebuilt and the grid re-colors live — no hardcoded hex.
fontFamily: 'inherit' keeps the grid on the app's skin-aware font.
The entrance animation — and why it animates translate, not transform
Every grid variant gets the same one-time staggered fade + rise as the rest of the app's entrances. But
AG Grid can't use motion/react per row — it virtualizes and recycles its own row DOM. So the entrance is
done in CSS, with one critical detail:
Animate the translate property, not transform
AG Grid positions every row with the CSS transform property. If the entrance also animated transform
it would clobber AG Grid's positioning and the rows would jump. Instead the keyframe animates the
separate translate property, which composes with transform — the row keeps AG Grid's position
and gets the entrance offset added on top.
@keyframes ag-row-in {
from { opacity: 0; translate: 0 16px; }
to { opacity: 1; translate: 0 0; }
}
.ag-rows-in .ag-row {
// Mirrors EASE_OUT ([0.16, 1, 0.3, 1]) from src/lib/motion.ts.
animation: ag-row-in 0.45s cubic-bezier(0.16, 1, 0.3, 1) both;
}
@for $i from 1 through 12 {
.ag-rows-in .ag-row:nth-child(#{$i}) { animation-delay: #{($i - 1) * 0.05}s; }
}The .ag-rows-in class is added once on onFirstDataRendered and removed after ~1 s so
scroll-recycled rows don't replay the entrance — packaged in the shared useRowStagger() hook, gated by
useReducedMotion():
function useRowStagger() {
const reduce = useReducedMotion()
const ref = useRef<HTMLDivElement>(null)
const onFirstDataRendered = useCallback(() => {
if (reduce) return
const el = ref.current
if (!el) return
el.classList.add('ag-rows-in')
window.setTimeout(() => el.classList.remove('ag-rows-in'), 1000)
}, [reduce])
return {ref, onFirstDataRendered}
}Columns, editing & CSV export
- Use
flexfor proportional widths andmaxWidthto cap narrow columns. - Format values with
valueFormatter; the shared currency formatter is(p) => `$${(p.value ?? 0).toLocaleString()}`. - Persisted-value i18n: raw values stay English in the data (
'Admin','Active'); display goes through render-time key maps (ROLE_KEY/STATUS_KEY→demo:keys) invalueFormatters and renderers. - Editing: mark columns
editable: true; constrain choices withagSelectCellEditor+values, numbers withagNumberCellEditor. Give an editable grid its own copy of the row array (useState(() => ROWS.map((r) => ({...r})))) so inline edits never mutate shared data. - CSV export (community-safe): capture the
GridApiinonGridReady, then callapi.exportDataAsCsv()from a toolbarButton.
To add a variant: add a grid component to grids.tsx (reuse useGridTheme + useRowStagger + ROWS), a
page wrapper in DataGridVariants.tsx, a lazy appRoutes entry, a menu leaf under the Data Grid group,
and the nav:/demo: i18n keys.
Best practices
- Never add
ag-grid-enterpriseor enterprise-only features. Keep the registry onAllCommunityModule. - Lazy-load grid pages (AG Grid is heavy) — all six pages come from one module, so they share one Vite chunk.
- Theme through tokens (
useChartTokens+themeQuartz.withParams), reusinguseGridTheme(). - Bound the grid's height (
style={{height}}) — AG Grid virtualizes and requires it. - Memoize
columnDefsand thethemeobject so the grid isn't torn down each render. - Animate
translate, nottransformfor any custom AG Grid row motion; gate it onuseReducedMotion(). - Custom renderers fill the cell (
flex h-full items-center) and guardrowPinned. - Copy the row array before enabling editing so inline edits don't mutate shared seed data.
- For a small, static dataset prefer the Basic Table — it's lighter and needs no grid lib.
Troubleshooting
| Symptom | Cause & fix |
|---|---|
| Grid area is blank / zero height | The wrapping element has no explicit height. Set style={{height}}. |
| Rows "jump" or mis-position during the entrance | Something is animating transform on .ag-row. Use the translate property instead. |
| Entrance replays while scrolling | The .ag-rows-in class wasn't removed. It must drop after ~1 s so recycled rows don't re-run the keyframe. |
| Grid colors don't match the active skin / dark mode | The theme was hardcoded instead of built from useChartTokens(). Use useGridTheme() (memoized on [tk]). |
| Custom cell content sits at the top of the row | Wrap the renderer in flex h-full items-center. |
| Action buttons appear on the total row | Renderers/formatters must check p.node.rowPinned and bail on pinned rows. |
| Inline edits change other grids' data | The editable grid shares the seed array. Give it its own copy (ROWS.map((r) => ({...r}))). |
| Console warning about modules not registered | ModuleRegistry.registerModules([AllCommunityModule]) is missing or ran too late — call it at module scope, once. |
FAQ
Why AG Grid Community and not a paid grid? It's MIT-licensed and ships cleanly to every ThemeForest buyer. CSV export is community — the Selection variant demos it.
Can I turn on row grouping / pivot / Excel export? No — those are enterprise-only. A buyer who needs them must license AG Grid Enterprise themselves.
Why six separate grid routes? Each variant demos one capability in isolation, which keeps every page focused and gives the sidebar's Data Grid group real depth. They share one source module, one dataset, one theme hook, and one Vite chunk — so the split costs nothing.
When should I use the Basic Table vs. the Data Grid? Small, mostly-read data → Basic Table (lighter, no grid lib). Larger, interactive data needing sort/filter/selection/editing/pinning → Data Grid.
Related
Charts
Shares the useChartTokens hook and the lazy-loading pattern.
Components
Badge, Avatar, Progress, Pagination used in the table cells.
Design tokens & dark mode
The --* tokens the grid theme reads.
Design skins
Why the grid re-colors on skin change.
Motion & effects
Stagger/StaggerItem, EASE_OUT, and reduced-motion.
Was this page helpful?
