npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

mosaic-data-table

v0.0.79

Published

React DataTable component with Material-UI

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.

Live demo →

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-table

Peer 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 const

Content-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

  • PaddingPluggin and SkeleonRows (old misspelled export names) still work but are deprecated — use PaddingPlugin and SkeletonRows.
  • Row identity: several plugins take onGetRowId — give them a stable id from your data.

License

MIT