mosaic-data-table
v0.0.79
Published
React DataTable component with Material-UI
Maintainers
Readme
Mosaic Data Table
A lightweight, plugin-based React data table for Material UI. The core renders a plain MUI <Table>; everything else — sorting, filtering, selection, pinned columns, row details, loading states — is a plugin you opt into. Use only what you need.
Features
- 🔌 Plugin architecture — the core stays small, features compose
- 📌 Pinned (sticky) columns, including responsive pinning by breakpoint
- 🔍 Filter row, column sorting, row selection, row details, row actions
- 💀 Skeleton loading and empty states
- 🎨 Material UI styling, themable via CSS variables
- 🧩 Write your own plugins against 23 documented hook points
- 🚀 Full TypeScript support
Installation
npm install mosaic-data-tablePeer dependencies: react / react-dom 18, @mui/material 6, @mui/icons-material 6, @emotion/react, @emotion/styled.
@mui/x-date-pickers 7 is only needed if you use the filter row with date / time / datetime filters.
Quick start
import { MosaicDataTable, useGridPlugins, CustomBodyCellContentRenderPlugin, ColumnDef } from 'mosaic-data-table';
const headCells: ColumnDef[] = [
{ id: 'id', header: 'ID', width: 80, cell: (row) => row.id },
{ id: 'name', header: 'Name', width: 200, cell: (row) => row.name },
];
const items = [
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'Jane Doe' },
];
function MyTable() {
const gridPlugins = useGridPlugins(
CustomBodyCellContentRenderPlugin, // renders each column's `cell` function
);
return (
<MosaicDataTable
plugins={gridPlugins}
headCells={headCells}
items={items}
/>
);
}CustomBodyCellContentRenderPlugin is what executes the cell function of your columns — include it (first) in virtually every table.
Core concepts
<MosaicDataTable> props
| Prop | Type | Description |
|---|---|---|
| headCells | ColumnDef[] | Column definitions (see below) |
| items | T[] \| null | Row data. null renders no body rows — useful together with SkeletonLoadingPlugin while loading |
| plugins | MosaicDataTablePlugin[] | The features you opt into |
| caption | string | Hidden table caption, for accessibility |
| …rest | BoxProps | Everything else (sx, className, …) is forwarded to the root container |
Column definition (ColumnDef)
| Field | Type | Description |
|---|---|---|
| id | string | Unique column id. Also the key used by filter/summary/highlight plugins |
| header | string \| () => string \| ReactNode | Header content |
| cell | (row) => ReactNode | Cell content (rendered by CustomBodyCellContentRenderPlugin) |
| width | number | Column width in px. Strongly recommended for pinned columns (offsets are computed from it; defaults to 40) |
| visible | boolean | Default true. Combine with useResponsiveHeadCellVisible({ breakpoint, direction }) to hide columns responsively |
| hasSort | boolean | Marks the column sortable (used by ColumnSortPlugin) |
| pin | 'left' \| 'right' \| true \| PinProps | Pin the column (used by PinnedColumnsPlugin). true pins on both sides; use createResponsivePin() for breakpoint-dependent pinning |
| highlight | boolean | Highlight the column (used by HighlightColumnPlugin) |
Plugins
Plugins are plain objects; the two hooks below just keep their identity stable across renders:
const gridPlugins = useGridPlugins(
CustomBodyCellContentRenderPlugin, // a static plugin object
usePluginWithParams(ColumnSortPlugin, { // a plugin factory + params, memoized by param values
order, orderBy, onSort,
}),
);Order matters. Plugins are applied sequentially: column-list plugins add their system columns in order, and content-render plugins wrap each other's output. A good baseline order is the one used in the demo: content render → filter row → sort → summary → padding → highlight → selection → row detail → fill-space → actions → pinned columns → skeleton → empty state.
Some plugins keep state in a small external store (createRowSelectionStore(), createRowDetailStore(), createFilterRowStore()). Create these once — useMemo(() => createRowDetailStore(), []) — and you can also drive them imperatively from outside the table (e.g. rowDetailStore.open(id)).
Built-in plugins
CustomBodyCellContentRenderPlugin
Renders each column's cell(row) function. No parameters — pass the object itself. Include it first.
ColumnSortPlugin
Sort indicators + click handling for columns with hasSort: true. Sorting itself is up to you — react to onSort.
usePluginWithParams(ColumnSortPlugin, {
order: 'asc', // 'asc' | 'desc'
orderBy: 'name', // string | null
onSort: (sortBy, sortOrder) => { /* re-query / re-sort your items */ },
})FilterRowPlugin
Adds a filter row under the header. Renders only while visible is true.
usePluginWithParams(FilterRowPlugin, {
visible: true,
key: 'filter_row', // unique key for this filter row
store: useMemo(() => createFilterRowStore(), []),
filterChanged: (filter) => { /* Record<columnId, { operator, value }> */ },
filterColumns: {
name: 'string', // shorthand: just the type
city: { type: 'string', ...DefaultStringFilterOptions }, // with operator menu
age: { type: 'number', ...DefaultNumberDateFilterOptions },
country: {
type: 'select',
selectOptions: [{ value: 'AT', label: 'Austria' }, /* … */],
},
registered: 'date', // needs @mui/x-date-pickers
},
})Filter types: 'string' | 'number' | 'boolean' | 'select' | 'date' | 'time' | 'datetime'. Passing an object instead of the type string lets you add an operator menu (operators, defaultOperator) — DefaultStringFilterOptions and DefaultNumberDateFilterOptions are ready-made sets. Date/time filters require your app to be wrapped in a LocalizationProvider from @mui/x-date-pickers.
RowSelectionPlugin
Adds a checkbox column (pinned left).
usePluginWithParams(RowSelectionPlugin, {
rowSelectionStore: useMemo(() => createRowSelectionStore(), []),
onGetRowId: (row) => row.id,
onSelectOne: (id) => { /* … */ },
onDeselectOne: (id) => { /* … */ },
visible: true, // optional, default true
})RowDetailPlugin
Expandable detail row per data row, with an expander column (pinned left).
const rowDetailStore = useMemo(() => createRowDetailStore(), []);
usePluginWithParams(RowDetailPlugin, {
rowDetailStore,
onGetRowId: (row) => row.id,
getExpansionNode: (row, params) => <div>Details for {row.name}</div>,
showExpanderButton: true, // optional, default true
})The store can be driven from anywhere: toggle(id), open(id), close(id), setParams(id, params) (opens the row and passes params to getExpansionNode), clear().
RowActionsPlugin
A ⋮ menu column (pinned right).
usePluginWithParams(RowActionsPlugin, {
actions: [
{ id: 'edit', render: (row) => <MenuItem key={`edit-${row.id}`}>Edit</MenuItem> },
{ id: 'remove', render: (row) => <MenuItem key={`remove-${row.id}`}>Remove</MenuItem>,
isVisible: (row) => !row.readonly },
],
})SummaryRowPlugin
Extra row appended after the data rows. Add several by using different keys.
usePluginWithParams(SummaryRowPlugin, {
key: 'totals',
summaryColumns: {
name: 'Total',
tokens: () => <b>{total}</b>, // string | ReactNode | function
},
})PinnedColumnsPlugin
Makes columns with a pin definition sticky while the table scrolls horizontally. No parameters:
usePluginWithParams(PinnedColumnsPlugin, {})// in your headCells:
{ id: 'name', pin: 'left', width: 200, /* … */ } // always pinned
{ id: 'tokens', pin: createResponsivePin(true, 'lg', 'up'), // pinned on both sides at ≥ lg
width: 100, /* … */ }createResponsivePin(pin, breakpoint, direction) activates the pin only when the breakpoint matches (direction 'up' | 'down', default 'down'). Give pinned columns an explicit width — sticky offsets are computed from it.
HighlightColumnPlugin / HighlightRowPlugin
usePluginWithParams(HighlightColumnPlugin, {
isColumnHighlighted: (headCellId) => headCellId === 'role',
})
// columns with `highlight: true` in their ColumnDef are highlighted too
usePluginWithParams(HighlightRowPlugin, {
isRowHighlighted: (row) => row.status === 'Inactive',
})SkeletonLoadingPlugin
Replaces the body with skeleton rows while loading (and temporarily unpins columns so the skeleton spans cleanly).
usePluginWithParams(SkeletonLoadingPlugin, {
isLoading,
rowsWhenEmpty: 5, // skeleton rows when there is no previous data
maxRowsWhenNotEmpty: 10, // cap when re-loading over existing data
})EmptyDataPlugin
Shown when items is an empty array (not when it is null).
usePluginWithParams(EmptyDataPlugin, { content: 'Wow, such empty!' })PaddingPlugin
Default cell padding, skipping the system columns.
usePluginWithParams(PaddingPlugin, {
skipCellHeads: ['sys_expansion', 'sys_selection', 'sys_actions'], // default
})ColumnsFillRowSpacePlugin
Appends an invisible flexible column so fixed-width columns don't stretch — keeps right-pinned action columns at the table edge. Pass the object itself, no parameters.
HideHeaderPlugin
Collapses the header row (it stays in the layout for column sizing). Pass the object itself, no parameters.
EventsPlugin
Click handlers for every part of the table. Row/cell handlers receive the row / columnDef on event.currentTarget.
usePluginWithParams(EventsPlugin, {
bodyRowOnClick: (event) => console.log(event.currentTarget.row),
bodyRowCellOnClick: (event) => console.log(event.currentTarget.columnDef, event.currentTarget.row),
// also: tableOnClick, bodyOnClick, headOnClick, headRowOnClick, headRowCellOnClick
})Theming
The table reads its colors from CSS variables, derived from your MUI palette (works out of the box with MUI's CSS theme variables — createTheme({ cssVariables: true })):
--mui-palette-MosaicDataTable-background /* cell background */
--mui-palette-MosaicDataTable-highlight /* highlighted rows/columns */
--mui-palette-MosaicDataTable-rowHover /* row hover */
--mui-palette-MosaicDataTable-extraRow /* filter/summary rows */Override any of them globally or per-table to restyle. The styled roots (MosaicDataTableRoot, MosaicDataTableRowRoot, MosaicDataTableCellRoot, …) are exported as well.
Writing your own plugin
A plugin is an object with a scope (one or several hook points) and the matching functions. Each function receives a single props object.
import { MosaicDataTableBodyCellStylePlugin } from 'mosaic-data-table';
export const RedCellPlugin: MosaicDataTableBodyCellStylePlugin = {
scope: 'body-cell-style',
getBodyCellStyle: ({ headcell, row, gridApi }) => {
return { backgroundColor: '#ff000070' };
},
};A plugin can implement several scopes at once — for example RowSelectionPlugin is both a grid-columns plugin (adds the checkbox column) and a body-cell-content-render plugin (renders the checkbox):
scope: ['grid-columns', 'body-cell-content-render'] as constContent-render plugins receive the output of the previous plugin as children — return children untouched for cells you don't handle, so the chain keeps working.
Available hook points
| Scope | Interface | Purpose |
|---|---|---|
| grid-columns | MosaicDataTableGridColumnsPlugin | Add / remove / transform columns |
| body-render | MosaicDataTableBodyRenderPlugin | Replace the whole table body |
| head-row-render / body-row-render | …HeadRowRenderPlugin / …BodyRowRenderPlugin | Replace a header / body row |
| head-cell-render / body-cell-render | …HeadCellRenderPlugin / …BodyCellRenderPlugin | Replace a header / body cell |
| head-cell-content-render / body-cell-content-render | …HeadCellContentRenderPlugin / …BodyCellContentRenderPlugin | Wrap / provide cell content (chained through children) |
| head-row-style / body-row-style | …HeadRowStylePlugin / …BodyRowStylePlugin | sx for rows |
| head-cell-style / body-cell-style | …HeadCellStylePlugin / …BodyCellStylePlugin | sx for cells |
| head-extra-row-start / head-extra-row-end | …HeadExtraRowStartPlugin / …HeadExtraRowEndPlugin | Extra rows around the header row (e.g. filter row) |
| body-extra-row-start / body-extra-row-end | …BodyExtraRowStartPlugin / …BodyExtraRowEndPlugin | Extra rows around the data rows (e.g. summary row) |
| table-props / body-props / head-props | …PropsPlugin / …BodyPropsPlugin / …HeadPropsPlugin | Extra props on <Table> / <TableBody> / <TableHead> |
| body-row-props / body-row-cell-props | …BodyRowPropsPlugin / …BodyRowCellPropsPlugin | Extra props on body rows / cells |
| head-row-props / head-row-cell-props | …HeadRowPropsPlugin / …HeadRowCellPropsPlugin | Extra props on header rows / cells |
The built-in plugins are the best reference implementations. Column-scoped content plugins can also set renderBodyCellContentColumnScope / renderHeadCellContentColumnScope to limit which columns they run for.
Notes
PaddingPlugginandSkeleonRows(old misspelled export names) still work but are deprecated — usePaddingPluginandSkeletonRows.- Row identity: several plugins take
onGetRowId— give them a stable id from your data.
License
MIT
