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

@vegux/ui-table

v0.1.3

Published

Data tables for the vegux design system: Material React Table under vegux tokens and components, with a standard table, a list table with filter panel and status bar, and a grouped table.

Readme

@vegux/ui-table

Data tables for the vegux design system. The package puts Material React Table (a table for React built on MUI and TanStack Table) under the vegux design tokens, and builds vegux components (Button, Pill, Pagination, Checkbox and more) into the table, so a table looks like the rest of a vegux app.

The code is public, but it is not open source. All rights are reserved. You may not copy, modify, redistribute or use the code except as vegux has agreed in writing. Read the LICENSE file in this package before you use it.

What you get

  • Table, the standard table: striped rows, sortable headers, a pagination footer, row selection, an empty state and loading rows. Column helpers draw the common cell types: textColumn, twoLineColumn, linkColumn, statusColumn and actionsColumn.
  • ListTable, for a page whose main content is one list: the table in a bordered card, with a section header, a toolbar with a Search filter panel and applied-filter chips, a status bar with counts, and pagination.
  • GroupedTable, for a lot of data, or data that comes in several groups (for example one group per material): a list of collapsible items, each with a table of its own that shows its first rows and a "Show +N more" strip, under the same toolbar and status bar.
  • The parts they are made of, which you can use on their own: TableToolbar, FilterToolbar, FilterPanel, StatusBar, TableSectionHeader, ResultsStatus and the useTableFilters hook.
  • VeguxTableProvider, which puts any Material React Table under the vegux look.

The tables are for the Web layout and follow the Light or Dark mode of @vegux/ui's ThemeProvider.

Install

npm install @vegux/ui @vegux/ui-table react react-dom @mui/material@7 @mui/icons-material@7 @mui/x-date-pickers@8 @emotion/react @emotion/styled material-react-table

The major versions are part of the command on purpose: without them npm installs the newest MUI (9 today), which this package does not accept yet, and the install stops with a peer dependency conflict (ERESOLVE). pnpm would only warn and then install MUI 9, so use the same command there (pnpm add ...).

The package is ESM only. For TypeScript, set moduleResolution to bundler, node16 or nodenext; the old node setting does not read the package's exports map. Set skipLibCheck to true in your tsconfig.json, because the declarations of material-react-table 3.2.1 do not type-check against @mui/x-date-pickers 8 (three TS2707 errors inside material-react-table/dist/index.d.ts). It was tested with Vite only. Like @vegux/ui, whose components import their own CSS files, it needs a bundler that can import CSS from node_modules; it cannot be loaded in plain Node.

Everything except @vegux/ui-table itself is a peer dependency, so your app installs it once and this package bundles none of it:

| Package | Range | What it is for | | -------------------------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------- | | @vegux/ui | ^0.1.1 | The components the tables are made of, and the ThemeProvider they read the mode from. | | react, react-dom | ^19.0.0 | React 19. | | material-react-table | ^3.2.1 | The table engine. | | @mui/material, @mui/icons-material | ^7.3.11 | Material React Table's MUI packages. The package was tested with 7.3.11, so MUI 8 and 9 are not accepted yet. | | @mui/x-date-pickers | ^8.29.3 | Material React Table loads it for its date filters, so it must be installed. The tables use none of the paid MUI X features. | | @emotion/react | ^11.14.0 | MUI's styling engine. | | @emotion/styled | ^11.14.1 | MUI's styling engine. |

Why peers and not dependencies: MUI and emotion keep their theme and style cache in React context, and @vegux/ui keeps the light or dark mode in one, so a second copy of any of them would have a second, empty context. With peers your app has exactly one copy of each and chooses the versions.

If you turn on Material React Table's date filters yourself, you also need a date library and MUI's LocalizationProvider; the tables in this package do not use them.

Set it up

Set up @vegux/ui first: import its tokens.css, wrap the app in ThemeProvider and load the Roboto font, as its own README describes. The font files are part of neither package; for example, install @fontsource/roboto and import the weights you use. The table text is set in the same --vg-typography-font-family token as the vegux components, so the tables and the components always share one font: with Roboto loaded both use it, and without it both fall back to the system sans-serif font. Then add three things:

  1. Import @vegux/ui-table/layers.css first, before every other stylesheet, including the ones your other imports load. The file holds one line, the order of the cascade layers, and the order must be fixed before any other layer is seen.
  2. Wrap the app in VeguxTableProvider, inside ThemeProvider. It renders no element of its own and adds no global styles.
  3. Use the tables.
// main.tsx
import '@vegux/ui-table/layers.css'; // first, before every other stylesheet
import '@vegux/ui/tokens.css';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { ThemeProvider } from '@vegux/ui';
import { VeguxTableProvider } from '@vegux/ui-table';
import { App } from './App';

const root = document.getElementById('root');
if (root === null) throw new Error('Missing #root element');

createRoot(root).render(
  <StrictMode>
    <ThemeProvider>
      <VeguxTableProvider>
        <App />
      </VeguxTableProvider>
    </ThemeProvider>
  </StrictMode>,
);

If TypeScript reports that it cannot find the module @vegux/ui-table/layers.css, your project has no type for CSS imports. In a Vite project, add /// <reference types="vite/client" /> to a .d.ts file; otherwise add declare module '*.css';.

Why the layers matter. MUI writes its styles with emotion as ordinary CSS that is not in a layer, and CSS that is not in a layer beats every @layer rule. @vegux/ui keeps all of its CSS in layers, so without help MUI would win on any element both style. VeguxTableProvider turns on MUI's own cascade-layer switch, which puts all MUI CSS in @layer mui, and layers.css places mui below the whole vg layer. On any element both style, the design system wins; your own CSS that is not in a layer still beats both.

If you do not import layers.css, the provider adds the same order statement to the start of <head> when it mounts, and the tables still work. Importing the file is better because the order is then fixed in the first paint and in server-rendered HTML. An app that already rendered an emotion tree with the layer switch before it mounted the provider should import the file.

VeguxTableProvider follows the mode of the ThemeProvider above it, including a nested Dark island. The mode prop forces a mode for a table that sits outside any ThemeProvider.

Frameworks

The package was tested with Vite only. It starts every file with the "use client" directive, so in a Next.js App Router project it can be imported from a Server Component. Emotion, which MUI styles with, needs MUI's cache provider there (AppRouterCacheProvider from @mui/material-nextjs); without it a test with Next.js 16.3.4 showed a hydration mismatch (React error 418). Nothing more than that was tested, so other frameworks and versions are untested.

The examples

Each example below is a complete file. They share one small data file.

// data.ts
import {
  actionsColumn,
  linkColumn,
  statusColumn,
  textColumn,
  type GroupedTableGroup,
  type TableStatus,
} from '@vegux/ui-table';
import type { MRT_ColumnDef } from 'material-react-table';

export interface Row {
  id: string;
  code: string;
  name: string;
  resource: string;
  status: TableStatus[];
  isFlagged: boolean;
  kind: 'default' | 'rework';
  url: string;
}

const row = (
  code: string,
  name: string,
  resource: string,
  status: TableStatus,
  isFlagged: boolean,
  kind: 'default' | 'rework',
): Row => ({
  id: code,
  code,
  name,
  resource,
  status: [status],
  isFlagged,
  kind,
  url: `/products/${code}`,
});

export const rows: Row[] = [
  row(
    '851520001',
    'Sunset Amber Vase',
    'C4541',
    { label: 'Approved', status: 'success' },
    false,
    'default',
  ),
  row(
    '520201124',
    'Ocean Breeze Candle',
    'C4541',
    { label: 'Pending Review', status: 'warning' },
    true,
    'rework',
  ),
  row(
    '151512410',
    'Maplewood Serving Tray',
    'C4610',
    { label: 'In Use', status: 'info' },
    false,
    'default',
  ),
  row(
    '955622001',
    'Crimson Velvet Pillow',
    'C4725',
    { label: 'Approved', status: 'success' },
    false,
    'default',
  ),
  row(
    '645123111',
    'Linen Table Runner',
    'C4610',
    { label: 'Draft', status: 'neutral' },
    false,
    'default',
  ),
  row(
    '721540011',
    'Copper Wire Lantern',
    'C4725',
    { label: 'Rejected', status: 'danger' },
    true,
    'rework',
  ),
  row(
    '336214510',
    'Stoneware Mug Set',
    'C4541',
    { label: 'Approved', status: 'success' },
    false,
    'default',
  ),
  row(
    '412100218',
    'Woven Storage Basket',
    'C4610',
    { label: 'Expired', status: 'danger' },
    false,
    'default',
  ),
];

export const columns: MRT_ColumnDef<Row>[] = [
  actionsColumn<Row>({
    actions: (item) => [
      { id: 'edit', label: 'Edit', onAction: () => window.alert(`Edit ${item.name}`) },
    ],
    label: (item) => `Actions for ${item.name}`,
  }),
  linkColumn<Row>({ accessorKey: 'code', header: 'Item Code', href: (item) => item.url }),
  textColumn<Row>({ accessorKey: 'name', header: 'Description', sortable: true }),
  textColumn<Row>({ accessorKey: 'resource', header: 'Resource', sortable: true }),
  statusColumn<Row>({ accessorKey: 'status', header: 'Status', fill: true }),
];

export interface Step {
  id: string;
  step: string;
  operation: string;
  duration: string;
}

const step = (id: string, operation: string, duration: string): Step => ({
  id,
  step: id,
  operation,
  duration,
});

export const groups: GroupedTableGroup<Step>[] = [
  {
    id: 'm1',
    title: 'Material: 50127022',
    subtitle: 'Description: Sunset Amber Vase, Version: v4',
    status: { label: 'Approved', status: 'success' },
    rows: [
      step('0010', 'Mix', '20 min'),
      step('0020', 'Heat', '45 min'),
      step('0030', 'Cool', '30 min'),
      step('0040', 'Pack', '15 min'),
      step('0050', 'Label', '10 min'),
    ],
  },
  {
    id: 'm2',
    title: 'Material: 50127101',
    subtitle: 'Description: Ocean Breeze Candle, Version: v1',
    status: { label: 'Pending Review', status: 'warning', shade: 2 },
    rows: [step('0010', 'Melt', '25 min'), step('0020', 'Pour', '15 min')],
  },
];

export const stepColumns: MRT_ColumnDef<Step>[] = [
  textColumn<Step>({ accessorKey: 'step', header: 'Step' }),
  textColumn<Step>({ accessorKey: 'operation', header: 'Operation' }),
  textColumn<Step>({ accessorKey: 'duration', header: 'Duration' }),
];

The standard table

Table is Material React Table with the vegux look. It renders its own VeguxTableProvider, so a provider around it is optional. Give it columns, data and a name (aria-label). Put a TableToolbar above it through the toolbar prop.

// StandardTable.tsx
import { Table, TableToolbar } from '@vegux/ui-table';
import { columns, rows } from './data';

export function StandardTable() {
  return (
    <Table
      aria-label="Products"
      columns={columns}
      data={rows}
      getRowId={(item) => item.id}
      enableRowSelection
      initialState={{ pagination: { pageIndex: 0, pageSize: 5 } }}
      toolbar={<TableToolbar onAddNew={() => window.alert('Add new')} />}
    />
  );
}

The column helpers take Material React Table's column options (accessorKey, header, size and so on) and add:

  • sortable, which turns sorting on for the column;
  • info and infoLabel, which add an info icon with a tooltip to the header;
  • fill, which lets the column take the width that is left.

twoLineColumn takes a sublabel(row) for the second line, linkColumn an href(row), and actionsColumn a list of actions(row) for the row menu (each with id, label, onAction and optionally intent: 'destructive' or isDisabled) and an optional label(row) for the button's accessible name.

Every other Material React Table option passes through (state, callbacks, initialState, enableRowSelection, ...) except the ones that only feed the toolbars, the pagination and the header controls the wrapper replaces (muiTopToolbarProps, paginationDisplayMode, enableColumnResizing and similar). Those are type errors, not silent no-ops; the type TableProps shows exactly which. The muiTablePaperProps, muiTableContainerProps, muiTableProps, muiTableHeadCellProps, muiTableBodyProps, muiTableBodyRowProps and muiTableBodyCellProps options are merged with the wrapper's own, with yours on top.

TableToolbar draws the row above the table: a Filter button, Download/Upload, column arrange, filter chips and Add New. It shows only the parts you give a handler or a value (onFilterPress, onDownloadUploadPress, onArrangeColumnsPress, filterTags, onAddNew): a button without its handler is not drawn at all. startSlot and endSlot add your own content.

Passing the handlers shows the buttons, and without a handler a button is not drawn. A list page design often draws Download/Upload and the column-arrange button although nothing is behind them (they are inert in the design), so a page that should match such a design passes onDownloadUploadPress and onArrangeColumnsPress (an empty function is enough until there is something to do), together with onFilterPress and onAddNew. A page that passes only onAddNew shows two buttons fewer than the design. The same props go in FilterToolbar and, for ListTable and GroupedTable, in toolbarProps.

Column widths

A column is 160 wide unless it sets size, and the Actions column is 80. A list page fixes the first columns and lets the last column fill the width that is left, so give the last column fill: true. Without a fill column the table is still as wide as its parent, but the browser spreads the spare width over every column in proportion: in a 1200 px wide table with an Actions column and three 160 columns, the Actions column grows from 80 to about 170 and the others from 160 to about 340.

Height

A table fills the height of the column it sits in only when it is the last thing in that column. The rule comes from the layout, so there is no prop to remember:

  • Last in the column (nothing after it in the same parent): the table grows into the room that is left, so the pager footer sits at the bottom, with empty space under the last row and not the footer hugging it. When there are more rows than fit, the rows scroll inside the table, the header stays at the top (it is sticky) and the footer stays at the bottom. ListTable does the same with its whole card: the card grows, the footer sits at the bottom of the card, and the rows scroll inside it.
  • Anything after it (a card, a section, a footer bar below the table): the table keeps its own height, the footer directly under the last row, and the page scrolls as usual. It does not take the room of the screen for itself.
  • GroupedTable follows the same rule, with one difference: it never shrinks below its own height, so a long list of items scrolls with the page and not inside the table. Its page footer sits at the bottom when there is room.

This works through flex layout, so the parent must be a flex column that has a height: the content area of the app shell (AppShell), a tab panel that is itself a flex column with flex: 1, or a flex column with a height of your own. In a parent that is not a flex container, or a flex column with no height to grow into, the table has nothing to grow into and hugs its content. (In a flex row the same rule would act along the row, so keep a table in a column.) The content area of AppShell is such a parent. A ListTable that is its last direct child fills it, and with more rows than fit its rows scroll inside the card. A standard Table that is its last direct child also grows to fill it, but with more rows than fit the shell's rule that its children never shrink keeps the table at its own height and the content area scrolls. To have the rows of a standard table scroll inside it, put the table in a flex column of your own that is flex: 1 1 auto; min-height: 0 in the content area. Elements that take no room but are still after the table in the same parent (an empty wrapper, say) count as "after it": put them before the table, or render them in a portal.

// A page area: a flex column with a height. The table is last, so it fills.
<div style={{ display: 'flex', flexDirection: 'column', height: 700 }}>
  <Table aria-label="Products" columns={columns} data={rows} />
</div>

// A section after the table: the table hugs its rows and footer.
<div style={{ display: 'flex', flexDirection: 'column', height: 700, overflowY: 'auto' }}>
  <Table aria-label="Products" columns={columns} data={rows} />
  <section>Notes</section>
</div>

A page in the app shell follows the same two cases. When the table is the last (or only) thing on the page, wrap it in a flex column that grows to the content area and may shrink to it, with the page's padding. A ListTable or a Table in it then fills the area, with the footer at the bottom, and the rows scroll inside under the sticky header. When more content comes under the table, the wrapper needs no height rule: the table keeps its own height and the content area scrolls.

// The table is last: the wrapper holds the height, the table fills it.
<AppShell sidebar={<MySidebar />} showFooter={false}>
  <div
    style={{
      display: 'flex',
      flexDirection: 'column',
      flex: '1 1 auto',
      minHeight: 0,
      padding: 'var(--vg-space-surface-padding)',
    }}
  >
    <ListTable title="Products" columns={columns} data={rows} />
  </div>
</AppShell>

// A section after the table: the table keeps its height and the page scrolls.
<AppShell sidebar={<MySidebar />} showFooter={false}>
  <div
    style={{
      display: 'flex',
      flexDirection: 'column',
      gap: 'var(--vg-space-stack-gap)',
      padding: 'var(--vg-space-surface-padding)',
    }}
  >
    <ListTable title="Products" columns={columns} data={rows} />
    <section>Notes</section>
  </div>
</AppShell>

The bottom footer of the shell (footer slot, showFooter) is for screens that move on to a next page, such as the steps of a form; list and table pages leave it off.

fillHeight makes it explicit and is the only override: true always grows (a table followed by a bar of its own, with the table taking what the bar leaves), false never grows (a table that is last in its column but must hug). Left out, the rule above applies. Table, ListTable and GroupedTable take it.

The header is sticky, so it stays in view while the rows scroll inside the table; enableStickyHeader: false turns that off. A maxHeight you set on the rows area (muiTableContainerProps={{ sx: { maxHeight: 400 } }}) wins over the layout, which is what row virtualization needs when the table is not in a column with a height.

Keyboard

Tab goes from control to control, and the cells are not stops. The sort buttons in the header, the info buttons, a row's action button, links, checkboxes and the pager buttons take focus in page order, so a list table of 7 columns and 8 rows takes 44 Tab presses from Filter to the first pager button, and its 63 cells add none. Enter or Space on a focused sort button sorts the column: each press goes ascending, descending, then unsorted.

This is Material React Table's enableKeyboardShortcuts set to false, which Table, ListTable and GroupedTable do for you. With the option on, every header and body cell is a tab stop so that the arrow keys, Home, End, Page Up and Page Down can move between cells, and a row of controls is buried under dozens of stops that have nothing to operate. If you want that cell navigation back (a table the user reads cell by cell, for instance), pass enableKeyboardShortcuts as true to Table, to ListTable and in the tableProps of GroupedTable.

The list table

ListTable takes the same columns and data, a title (which also names the table) and, optionally, the filter fields and the status items. Without filterFields there is no Filter button; without statusItems there is no status bar.

// ListPage.tsx
import { ListTable, TABLE_STATUS_BAR_ITEMS, type TableFilterFieldRows } from '@vegux/ui-table';
import { columns, rows, type Row } from './data';

// The Search filter panel: rows of fields.
const filterFields: TableFilterFieldRows<Row> = [
  [
    {
      id: 'item',
      type: 'text',
      label: 'Item Code/ Description',
      getValue: (item) => [item.code, item.name],
    },
    {
      id: 'resource',
      type: 'select',
      label: 'Resource',
      options: [
        { id: 'C4541', label: 'C4541' },
        { id: 'C4610', label: 'C4610' },
        { id: 'C4725', label: 'C4725' },
      ],
    },
    { id: 'isFlagged', type: 'switch', label: 'Show flagged', onLabel: 'Yes' },
  ],
  [
    {
      id: 'kind',
      type: 'radio',
      label: 'Type',
      options: [
        { id: 'default', label: 'Default' },
        { id: 'rework', label: 'Rework' },
      ],
    },
  ],
];

export function ListPage() {
  return (
    <ListTable
      title="List of Items"
      columns={columns}
      data={rows}
      getRowId={(item) => item.id}
      filterFields={filterFields}
      statusItems={TABLE_STATUS_BAR_ITEMS}
      toolbarProps={{
        // A button shows only when its handler is given. A list page design
        // often draws Download/Upload and Arrange columns although nothing
        // is behind them, so they are passed here as functions that do nothing.
        onDownloadUploadPress: () => {},
        onArrangeColumnsPress: () => {},
        onAddNew: () => window.alert('Add new'),
      }}
    />
  );
}
  • Filter opens the Search filter under the toolbar. It is a panel with no scrim, so the page behind it stays usable, and a click outside does not close it. It covers the status bar completely, also when the bar's Pills wrap onto more lines than the panel is tall. Escape closes it and focus returns to Filter. Search applies the values and shows them as chips, "Field: value", three at most, then "+ N More" and "Clear All".
  • A filter field matches a row on the row's key of the same name as the field id. Set getValue to match on something else (it may return an array, and the row matches when any entry matches) or columnId to name another key. A text field matches when the value contains the text, ignoring case; a select or radio field when the value equals the chosen option's id; a switch keeps only the rows whose value is true while it is on.
  • The status bar selects one status at a time, and All means no status filter. Clicking the selected status again removes the filter and selects All; clicking All while it is selected does nothing. onStatusChange gets the new id, which is the All id (the first item's) after a second click. The selected Pill is drawn in the Pill's Fill look, a tint of its colour with a check mark before the label, and the others are Soft. The Pills are toggle buttons in a group (aria-pressed), so Tab reaches each one and Space or Enter toggles it; the check mark is decoration and is hidden from screen readers. Each status shows how many rows have it among the rows that pass the filters. A row's status labels come from its status key (a label, or a list of labels or { label, status } objects, which is what statusColumn shows); getRowStatus reads them from somewhere else. TABLE_STATUS_BAR_ITEMS is a ready list of twelve items, All and eleven statuses; pass your own list of { id, label, status, shade } items to change it.
  • The filters and the status are controlled (filters with onFiltersChange, status with onStatusChange) or uncontrolled (defaultFilters, defaultStatus).
  • Keyboard and focus. Tab reaches every control in the panel; past the last one it closes the panel and moves on to the status bar. After a chip's dismiss, focus goes to the next chip, and to Filter after the last chip or Clear All. After a filter or status change a screen reader hears the number of results (resultsLabel changes the text).
  • The English strings are props: resultsLabel, statusBarProps, and toolbarProps (panelLabel, panelProps for the Search and Clear All buttons, chipLimit, addNewLabel, ...).
  • Large data. Keep pagination on: ListTable and GroupedTable page by default. In our tests (Chromium, 1440 px wide), 2,000 rows paged mounted in about 130 ms, and unpaged in about 10 s. Row virtualization (enableRowVirtualization with a maxHeight on the container) mounted them in about 0.3 s.

The grouped table

GroupedTable is for data that comes in groups. Each group is a collapsible item with a title, an optional subtitle and status Pill, and a table of its own rows. All groups share the same columns. The first group starts open.

// GroupedPage.tsx
import { GroupedTable, TABLE_STATUS_BAR_ITEMS } from '@vegux/ui-table';
import { groups, stepColumns, type Step } from './data';

export function GroupedPage() {
  return (
    <GroupedTable<Step>
      title="List of Materials"
      groups={groups}
      columns={stepColumns}
      tableProps={{ getRowId: (item) => item.id }}
      statusItems={TABLE_STATUS_BAR_ITEMS}
      groupActions={(group) => [
        { id: 'edit', label: 'Edit', onAction: () => window.alert(`Edit ${group.title}`) },
      ]}
    />
  );
}
  • An open item shows its first three rows and a "Show +N more" strip (previewRowCount and showMoreLabel change it). The items are paged at the bottom of the list (pageSize, enablePagination).
  • expandedIds with onExpandedChange (or defaultExpandedIds) controls which items are open.
  • filterFields work as in the list table, but a field's getValue receives the group. getGroupStatus reads a group's status labels (by default the label of its status).
  • Keep the default pageSize and do not open hundreds of items at once: an unpaged list with many open items is slow in the same way an unpaged ListTable is.

The filter parts on their own

ListTable and GroupedTable wire the parts together. To build your own page around a standard Table, use FilterToolbar and StatusBar with useTableFilters, which keeps the applied filters and filters your rows for you.

// FilteredTable.tsx
import { useState } from 'react';
import {
  FilterToolbar,
  StatusBar,
  TABLE_STATUS_BAR_ITEMS,
  Table,
  defaultRowStatus,
  matchesStatusBarItem,
  statusBarItemsWithCounts,
  useTableFilters,
  type TableFilterFieldRows,
} from '@vegux/ui-table';
import { columns, rows, type Row } from './data';

const fieldRows: TableFilterFieldRows<Row> = [
  [
    { id: 'name', type: 'text', label: 'Description' },
    { id: 'code', type: 'text', label: 'Item Code' },
  ],
];

export function FilteredTable() {
  const { filters, applyFilters, filterRows } = useTableFilters<Row>({ fieldRows });
  const [status, setStatus] = useState('all');

  const filtered = filterRows(rows);
  const shown = filtered.filter((item) =>
    matchesStatusBarItem(defaultRowStatus(item), TABLE_STATUS_BAR_ITEMS, status),
  );
  const items = statusBarItemsWithCounts(TABLE_STATUS_BAR_ITEMS, filtered, defaultRowStatus);

  return (
    <>
      <FilterToolbar fieldRows={fieldRows} filters={filters} onFiltersChange={applyFilters}>
        <StatusBar
          items={items}
          value={status}
          onChange={setStatus}
          aria-label="Filter products by status"
        />
      </FilterToolbar>
      <Table aria-label="Filtered products" columns={columns} data={shown} />
    </>
  );
}

Put the FilterToolbar above the Table, not in the table's toolbar prop: the table's paper clips the open panel. Pass the StatusBar as the FilterToolbar's children, as above: the open Search filter then covers the bar down to its last pixel, however many lines its Pills wrap onto (a panel with one row of fields is only about 80 tall, which a wrapped bar outgrows). The StatusBar can still stand on its own under the toolbar, and the panel then only hugs its own content. FilterToolbar also takes the TableToolbar options (onAddNew, onDownloadUploadPress, ...), chipLimit, and isOpen with onOpenChange to control the panel.

useTableFilters also returns columnFilters (the applied filters as TanStack column filters, for state.columnFilters once withTableFilterFns has given the columns the fields' rules), tags, removeFilter and clearFilters. filterRows filters on every field, including one that has a getValue and no column.

Material React Table directly

Any Material React Table can sit under the vegux look. Call useMaterialReactTable in a component that is rendered inside VeguxTableProvider: the hook reads the MUI theme when it runs, so a hook called above the provider computes its colours from MUI's default theme, which is Light, even when the page is Dark.

// PlainGrid.tsx
import {
  MaterialReactTable,
  useMaterialReactTable,
  type MRT_ColumnDef,
} from 'material-react-table';
import { rows, type Row } from './data';

const columns: MRT_ColumnDef<Row>[] = [
  { accessorKey: 'code', header: 'Item Code' },
  { accessorKey: 'name', header: 'Description' },
];

export function PlainGrid() {
  const table = useMaterialReactTable({ columns, data: rows });
  return <MaterialReactTable table={table} />;
}

Everything that is exported

| Export | What it does | | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | VeguxTableProvider | Applies the MUI theme for the current vegux mode, turns on MUI's cascade layer and points MUI's menus and tooltips at the vegux themed container. | | createVeguxTableTheme(mode, options?) | The MUI theme: a palette per mode, var(--vg-...) typography and component overrides. | | ensureVeguxTableLayerOrder(), VEGUX_TABLE_LAYER_ORDER | The layer order statement, and the run-time fallback for apps that do not import layers.css. | | @vegux/ui-table/layers.css | The same statement as a stylesheet. | | Table | The standard table: header, striped rows, dividers, sort, Info, pagination footer, empty state, loading rows and selection. | | textColumn, twoLineColumn, linkColumn, statusColumn, actionsColumn | Column helpers for the cell types. | | TableToolbar | The toolbar above a table (Filter, Download/Upload, column arrange, filter chips, Add New), driven by props. | | TABLE_HEADER_FILL and the other TABLE_* constants | The values the table is drawn with. | | ListTable | The list table, in its card. | | GroupedTable | The grouped table: collapsible items, each with its own table, and a page footer. | | FilterPanel, FilterToolbar | The Search filter panel, and the toolbar that opens it and shows the applied filters as chips. | | useTableFilters, withTableFilterFns and the other filter helpers | The applied filters (controlled or uncontrolled), their chips, and their TanStack column filters. | | StatusBar, TABLE_STATUS_BAR_ITEMS | The status quick filters with counts: one selected, and a second click on it removes the filter. | | ResultsStatus, defaultResultsLabel | The visually hidden role="status" message that says how many results a filter or status change left. | | TableSectionHeader | The bar that names a table on a page. |

Nothing here re-exports Material React Table: import its hooks and types from material-react-table.

Not yet

These are not in this release:

  • merged cells (row spans), the hybrid table that mixes merged and ordinary rows, and the editable table;
  • a density option, column pinning, column resizing and horizontal scroll;
  • a Mobile table (stacked cards for small screens);
  • doc comments in the type declarations: they describe every prop's type but carry no comments, as in @vegux/ui.

The tables' own English strings ("Action", "Row actions", the toolbar labels, "Show +N more", the result counts) are props; Material React Table's strings are its English defaults.

Versions

This is a 0.x release. Until 1.0, a minor version (0.1 to 0.2) can change or remove parts of the API. Pin an exact version in your package.json and read the changes before you upgrade. The @vegux/ui peer range follows the same rule: ^0.1.1 accepts 0.1.1 and later 0.1.x only.