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

bimatter-react-table

v0.1.28

Published

React table components for data-heavy BIM and engineering interfaces.

Readme

bimatter-react-table

React table components for data-heavy BIM and engineering interfaces.

bimatter-react-table provides BimatterTable: a typed React table component with column resizing, sorting, row selection, column and row reordering, tree-like grouping, calculated totals, sticky header/footer behavior, and custom rendering hooks.

The default visual style is inspired by Mantine's clean table UI, but Mantine is not required.

Installation

npm install bimatter-react-table

Basic Usage

import {
    BimatterTable,
    type BimatterTableColumn,
    type BimatterTableRowId,
} from "bimatter-react-table";
import "bimatter-react-table/style.css";
import { useState } from "react";

type FileRecord = {
    id: number;
    name: string;
    owner: string;
    status: "Ready" | "Review" | "Draft";
    size: number;
    triangles: number;
    updatedAt: string;
};

const columns: BimatterTableColumn<FileRecord>[] = [
    {
        key: "name",
        title: "File",
        width: 320,
        minWidth: 180,
    },
    {
        key: "owner",
        title: "Owner",
        width: 220,
    },
    {
        key: "status",
        title: "Status",
        width: 140,
    },
    {
        key: "size",
        title: "Size",
        type: "fileSize",
        unit: "MB",
        calc: true,
        width: 140,
    },
    {
        key: "triangles",
        title: "Triangles",
        type: "number",
        unit: "pcs",
        calc: true,
        width: 180,
    },
    {
        key: "updatedAt",
        title: "Updated",
        type: "date",
        width: 160,
    },
];

const records: FileRecord[] = [
    {
        id: 1,
        name: "Architecture.ifc",
        owner: "Anna Lebedeva",
        status: "Ready",
        size: 42.8,
        triangles: 1240033,
        updatedAt: "2026-07-09",
    },
];

export function FilesTable() {
    const [selectedRowIds, setSelectedRowIds] = useState<BimatterTableRowId[]>(
        [],
    );

    return (
        <BimatterTable
            columns={columns}
            records={records}
            resizable
            sortable
            selectable
            selectedRowIds={selectedRowIds}
            highlightOnHover
            onSelectionChange={({ selectedRowIds }) =>
                setSelectedRowIds(selectedRowIds)
            }
            rowClickHandle={({ record, event }) =>
                console.log("Row click", record, event)
            }
        />
    );
}

BimatterTable

<BimatterTable columns={columns} records={records} />

Props

| Prop | Type | Default | Description | | ----------------------- | ----------------------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | | columns | BimatterTableColumn<TRecord>[] | required | Column definitions. | | records | TRecord[] | [] | Table records. | | recordsm | TRecord[] | [] | Compatibility alias for records. If records is provided, it takes priority. | | resizable | boolean | false | Enables column width resizing. | | resisable | boolean | false | Compatibility alias for resizable. | | sortable | boolean | false | Enables column sorting controls in the header. | | movableColumns | boolean | false | Enables drag and drop column reordering. | | movableRows | boolean | false | Enables drag and drop row reordering. | | fixFirstColumn | boolean | false | Keeps the first visible column sticky on the left. The fixed first column cannot be dragged. | | fixGroupedColumn | boolean | false | Keeps the generated grouping column sticky on the left when grouping is active. | | countGrouped | boolean | false | Appends the group record count to each group label, for example File : Model.ifc (100). | | collapseGrouped | boolean | false | Collapses all groups by default when grouping is active. | | duplicateGroupValue | boolean | false | Repeats the deepest parent group value in the grouping column for each regular child row. | | toggleCollapse | (toggle) => void | undefined | Receives an imperative toggle function that collapses all groups or expands all groups. | | selectable | boolean | false | Enables row selection. | | useSelectionCheckBox | boolean | false | Adds a checkbox column at the start of each row and enables checkbox-based selection. | | selectedRowIds | BimatterTableRowId[] | undefined | Controlled selected row ids. | | defaultSelectedRowIds | BimatterTableRowId[] | [] | Initial selected row ids for uncontrolled selection. | | getRowId | (rowData, rowIndex) => string \| number | rowData.id ?? rowIndex | Returns a stable row id. | | isPlaceholderRecord | (rowData, rowIndex) => boolean | service flags | Marks technical records that create empty groups but are not rendered as regular rows. | | onSelectionChange | (data) => void | undefined | Called when row selection changes. | | onRowClick | (rowData, event, rowIndex) => void | undefined | Called on row click. | | onRowDoubleClick | (rowData, event, rowIndex) => void | undefined | Called on row double click. | | onContextMenu | (rowData, event, rowIndex) => void | undefined | Called on row context menu. | | onGroupRowClick | (groupData, event) => unknown | undefined | Called when a grouped row is clicked. Receives { name, children } and the click event. | | rowClickHandle | ({ record, event, rowIndex, rowId }) => void | undefined | Object-style row click handler. clickHandle is also supported as an alias. | | rowDoubleClickHandle | ({ record, event, rowIndex, rowId }) => void | undefined | Object-style row double click handler. doubleClickHandle is also supported as an alias. | | contextMenuHandle | ({ record, event, rowIndex, rowId }) => void | undefined | Object-style row context menu handler. | | useDragAndDropRows | (fromRow, toRow) => unknown | undefined | Enables dragging one row onto another row and calls the callback with source and target records. | | searchBy | string[] | undefined | Shows a search input above the table and searches through the provided column keys, ids, or string accessors. | | virtualized | boolean | false | Enables row virtualization with react-virtualized for large datasets. | | virtualizedBodyHeight | number | auto | Fixed virtualized body height in pixels. When omitted, the body height is calculated from the table container and updated on resize. | | virtualizedRowHeight | number | 44 | Fixed row height used by the virtualized grid. | | virtualizedOverscanRowCount | number | 8 | Number of extra rows rendered above and below the visible viewport. | | noHeader | boolean | false | Hides the table header. Header-based controls such as sorting, resizing, and column drag handles are not visible without the header. | | hideGrouped | boolean | false | Hides columns used for grouping from the regular column list. | | highlightOnHover | boolean | false | Enables row hover highlighting. | | highlightOnHoverColor | string | undefined | Overrides the row hover color. | | rowRender | (rowData) => ReactNode | undefined | Custom row content renderer. If it returns null or undefined, the default row renderer is used. | | columnRender | (columnData) => ReactNode | undefined | Custom header content renderer. If it returns null or undefined, the default header renderer is used. | | cellRender | (cellData) => ReactNode | undefined | Custom cell renderer. | | ceilRender | (ceilData) => ReactNode | undefined | Compatibility alias for cellRender. It is checked before cellRender. | | emptyContent | ReactNode | built-in empty message | Content rendered when there are no records. |

Columns

type BimatterTableColumn<TRecord extends object = Record<string, unknown>> = {
    id?: string;
    key?: Extract<keyof TRecord, string> | string;
    accessor?: BimatterTableColumnAccessor<TRecord>;
    title?: ReactNode;
    width?: number | string;
    minWidth?: number;
    maxWidth?: number;
    groupLevel?: number;
    hideGroupName?: boolean;
    showGroupParamName?: boolean;
    type?: "string" | "number" | "date" | "length" | "weight" | "fileSize";
    unit?: string;
    calc?: boolean;
    culc?: boolean;
    align?: "left" | "center" | "right";
    className?: string;
    headerClassName?: string;
    render?: (rowData: TRecord, rowIndex: number) => ReactNode;
};

Column Fields

| Field | Description | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------- | | id | Explicit column id. Used for sorting, drag and drop, grouping, and width state. | | key | Record field key. Also used as the column id if id is not provided. | | accessor | Record field key or function used to read a cell value. | | title | Header content. | | width | Base column width. Numbers are treated as pixels. Pixel strings such as "120px" are also used for initial width calculations. | | minWidth | Minimum column width in pixels. | | maxWidth | Maximum column width in pixels. | | groupLevel | Positive integer grouping level. Any number of grouping levels is supported. | | hideGroupName | Compatibility inverse alias for showGroupParamName: false. | | showGroupParamName | Shows the grouped column name before the value. Defaults to true; set to false to render only the group value. | | type | Value type used by default formatting and calculated totals. | | unit | Unit appended to numeric and dimension-like values. | | calc | Enables totals for numeric columns and shared-value summaries for string columns. | | culc | Compatibility alias for calc. | | align | Cell alignment. Defaults to left for every column type. | | className | Class name applied to body, group, and footer cells for this column. | | headerClassName | Class name applied to the header cell. | | render | Column-level cell renderer. It has priority over cellRender and ceilRender. |

Accessors

type BimatterTableColumnAccessor<TRecord extends object> =
    | Extract<keyof TRecord, string>
    | string
    | ((rowData: TRecord, rowIndex: number) => unknown);

If accessor is not provided, the component reads the value by column.key, then by column.id.

Value Types

| type | Default formatting | Default unit behavior | | ---------- | ----------------------------- | ----------------------- | | string | Default React value rendering | none | | number | Intl.NumberFormat | none | | date | Intl.DateTimeFormat | none | | length | Formatted number + unit | built-in length unit | | weight | Formatted number + unit | built-in weight unit | | fileSize | Formatted number + unit | built-in file-size unit |

Override the unit with unit:

{
    key: "triangles",
    title: "Triangles",
    type: "number",
    unit: "pcs",
    calc: true,
}

Sorting

<BimatterTable columns={columns} records={records} sortable />

When sortable is enabled, each header cell gets a sort control with three states:

| State | Behavior | | --------- | ------------------- | | default | No sorting. | | asc | Ascending sorting. | | desc | Descending sorting. |

Click order: default -> asc -> desc -> default.

Row Selection

const [selectedRowIds, setSelectedRowIds] = useState<BimatterTableRowId[]>([]);

<BimatterTable
    columns={columns}
    records={records}
    selectable
    useSelectionCheckBox
    selectedRowIds={selectedRowIds}
    onSelectionChange={({ selectedRowIds }) =>
        setSelectedRowIds(selectedRowIds)
    }
/>;

Selection behavior:

  • click a row to toggle it when selectable is enabled without useSelectionCheckBox;
  • click a row checkbox to toggle it when useSelectionCheckBox is enabled;
  • click the table container parent itself to clear selection;
  • use Shift + click to select the range between the last selected row and the clicked row or checkbox;
  • browser text selection is prevented during Shift + click.

Selection Change Data

type BimatterTableSelectionChangeData<TRecord extends object> = {
    selectedRowIds: BimatterTableRowId[];
    selectedRows: TRecord[];
    changedRow?: TRecord;
    changedRowId?: BimatterTableRowId;
    changedRows?: TRecord[];
    changedRowIds?: BimatterTableRowId[];
    reason?: "toggle" | "range" | "clear";
};

Row Events

type BimatterTableRowEventData<TRecord extends object> = {
    record: TRecord;
    rowData: TRecord;
    event: MouseEvent<HTMLTableRowElement>;
    rowIndex: number;
    rowId: BimatterTableRowId;
};

type BimatterTableRowEventHandler<TRecord extends object> = (
    data: BimatterTableRowEventData<TRecord>,
) => void;

type BimatterTableRowHandler<TRecord extends object> = (
    rowData: TRecord,
    event: MouseEvent<HTMLTableRowElement>,
    rowIndex: number,
) => void;

type BimatterTablePlaceholderRecordPredicate<TRecord extends object> = (
    rowData: TRecord,
    rowIndex: number,
) => boolean;
<BimatterTable
    columns={columns}
    records={records}
    rowClickHandle={({ record, event }) => {
        console.log("click", record, event);
    }}
    rowDoubleClickHandle={({ record, event }) => {
        console.log("double click", record, event);
    }}
    contextMenuHandle={({ record, event }) => {
        event.preventDefault();
        console.log("context menu", record);
    }}
/>

The legacy positional handlers onRowClick, onRowDoubleClick, and onContextMenu are also supported.

Column and Row Reordering

<BimatterTable columns={columns} records={records} movableColumns movableRows />

movableColumns enables drag and drop column reordering from the column title area. When fixFirstColumn is enabled, the first visible column cannot be dragged, and other columns cannot be dropped onto it.

movableRows adds a row drag handle. Dropping a row changes the visible row order and clears active sorting so the manual order is visible.

For row-to-row drops controlled by your app, pass useDragAndDropRows:

<BimatterTable
    columns={columns}
    records={records}
    useDragAndDropRows={(fromRow, toRow) => {
        console.log("drop", fromRow, "onto", toRow);
    }}
/>

Search

Pass searchBy with column keys, ids, or string accessors to show the search input above the table:

<BimatterTable
    columns={columns}
    records={records}
    searchBy={["name", "owner", "status"]}
/>

Typing in the input scrolls the table to the first match. The up and down buttons move to the previous or next match. With virtualized, navigation uses the virtualized grid scroll API.

Virtualization

For large datasets, enable row virtualization:

<BimatterTable
    columns={columns}
    records={records}
    virtualized
    virtualizedRowHeight={44}
/>

Virtualization is implemented with react-virtualized. It virtualizes rows while keeping BimatterTable sorting, grouping, selection, resizing, totals, custom row rendering, and row event handlers. Columns are not virtualized because table column counts are usually small and fixed column widths need to stay aligned with the header and footer.

By default, the virtualized body height is calculated from the table container height and updates on resize. Pass virtualizedBodyHeight only when you need a fixed body height.

Grouping

Grouping is configured on columns with groupLevel.

const columns: BimatterTableColumn<FileRecord>[] = [
    {
        key: "status",
        title: "Status",
        groupLevel: 1,
        showGroupParamName: false,
    },
    {
        key: "owner",
        title: "Owner",
        groupLevel: 2,
    },
];

Use positive integer levels (1, 2, 3, and so on). The table renders a tree-like row structure with an indent for each nested level. Groups can be expanded and collapsed.

Set showGroupParamName: false on a grouped column to render only the group value in the grouping column. For example, "Ready" is shown instead of "Status : Ready". hideGroupName: true is also supported as a compatibility alias.

Use hideGrouped to hide grouped columns from the regular column list:

<BimatterTable columns={columns} records={records} hideGrouped />

Use fixGroupedColumn to keep the generated grouping column sticky on the left:

<BimatterTable columns={columns} records={records} fixGroupedColumn />

Use countGrouped to append the group size to the group label:

<BimatterTable columns={columns} records={records} countGrouped />

With countGrouped, a group label can be rendered as "Status : Ready (100)". If the grouped column also has showGroupParamName: false, the same group is rendered as "Ready (100)".

Use duplicateGroupValue to repeat the deepest group value in the generated grouping column for every regular child row:

<BimatterTable columns={columns} records={records} duplicateGroupValue />

Use collapseGrouped to render all groups collapsed by default:

<BimatterTable columns={columns} records={records} collapseGrouped />

Register toggleCollapse to control all groups from outside the table:

const toggleGroupsRef = useRef<(() => void) | null>(null);

return (
    <>
        <button type="button" onClick={() => toggleGroupsRef.current?.()}>
            Toggle groups
        </button>

        <BimatterTable
            columns={columns}
            records={records}
            toggleCollapse={(toggleGroups) => {
                toggleGroupsRef.current = toggleGroups;
            }}
        />
    </>
);

Grouped rows can be handled with onGroupRowClick:

<BimatterTable
    columns={columns}
    records={records}
    onGroupRowClick={(group, event) => {
        console.log(group.name, group.children, event);
    }}
/>
type BimatterTableGroupRowData<TRecord extends object = Record<string, unknown>> = {
    name: string;
    children: Array<BimatterTableGroupRowData<TRecord> | TRecord>;
};

name matches the visible group label, for example "Status : Ready". children contains nested group data when another group level exists, otherwise it contains the source records for that group.

If grouped data contains calculated columns, group subtotals are rendered in the group row itself, aligned with the calculated columns. With multiple grouping levels, each level gets its own subtotal.

Placeholder Records For Empty Groups

Use isPlaceholderRecord when a record should create a group but must not be treated as a table row:

type WorkRecord = {
    id: string;
    workName: string;
    elementName?: string;
    isEmptyWorkPlaceholder?: boolean;
};

const columns: BimatterTableColumn<WorkRecord>[] = [
    { key: "workName", title: "Work", groupLevel: 1 },
    { key: "elementName", title: "Element", width: 240 },
];

const records: WorkRecord[] = [
    { id: "wall-1", workName: "Walls", elementName: "Wall A" },
    {
        id: "empty-finishing",
        workName: "Finishing",
        isEmptyWorkPlaceholder: true,
    },
];

<BimatterTable
    columns={columns}
    records={records}
    countGrouped
    isPlaceholderRecord={(record) => Boolean(record.isEmptyWorkPlaceholder)}
/>;

Finishing is rendered as a group row with count 0. The placeholder record is not rendered as a child row, is not selectable, is excluded from row events, search matches, totals and subtotals, and is not returned as a child record from onGroupRowClick.

If isPlaceholderRecord is omitted, records with __bimatterPlaceholder: true or __bimatterTablePlaceholder: true are treated as placeholder records.

Totals

Set calc: true on a numeric or string column to calculate totals.

{
    key: "size",
    title: "Size",
    type: "fileSize",
    unit: "MB",
    calc: true,
}

Totals are supported for:

  • string (renders the shared value when all records match, otherwise Varies);
  • number;
  • length;
  • weight;
  • fileSize.

The total row is rendered as a sticky footer. When grouping is active, group subtotals are shown in each group row above the calculated columns.

Custom Rendering

All render props are optional. If a renderer is not provided, or returns null or undefined, the table falls back to the default renderer.

column.render

Column-level cell renderer. It has the highest priority for body cells.

{
    key: "status",
    title: "Status",
    render: (rowData) => <StatusBadge status={rowData.status} />,
}

cellRender

Global cell renderer.

<BimatterTable
    columns={columns}
    records={records}
    cellRender={({ columnData, value }) => {
        if (columnData.key === "updatedAt" && typeof value === "string") {
            return (
                <time dateTime={value}>
                    {new Date(value).toLocaleDateString()}
                </time>
            );
        }

        return null;
    }}
/>

Cell render data:

type BimatterTableCellRenderData<TRecord extends object> = {
    rowData: TRecord;
    columnData: BimatterTableColumn<TRecord>;
    value: unknown;
    rowIndex: number;
    columnIndex: number;
    selected: boolean;
};

ceilRender is also supported as a compatibility alias. If both ceilRender and cellRender are provided, ceilRender is checked first.

columnRender

Custom header content renderer.

<BimatterTable
    columns={columns}
    records={records}
    columnRender={({ columnData }) => <span>{columnData.title}</span>}
/>

Column render data:

type BimatterTableColumnRenderData<TRecord extends object> = {
    columnData: BimatterTableColumn<TRecord>;
    columnIndex: number;
};

rowRender

Custom row content renderer. The returned ReactNode is rendered inside the table <tr>, so it should usually return one or more <td> elements.

When virtualized is enabled, rowRender is still supported. Returned <td> and <th> elements are converted to virtualized row cells with the configured column widths. If you return a custom component instead, it is rendered as the content area of the virtualized row.

<BimatterTable
    columns={columns}
    records={records}
    rowRender={({ cells }) =>
        cells.map((cell) => (
            <td key={String(cell.columnData.key)}>{String(cell.value)}</td>
        ))
    }
/>

Row render data:

type BimatterTableRowRenderData<TRecord extends object> = {
    rowData: TRecord;
    rowId: BimatterTableRowId;
    rowIndex: number;
    selected: boolean;
    columns: BimatterTableColumn<TRecord>[];
    cells: BimatterTableCellRenderData<TRecord>[];
};

Fixed First Column

<BimatterTable columns={columns} records={records} fixFirstColumn />

The first visible column becomes sticky on the left. If column reordering is enabled, the first visible column remains locked.

Empty State

<BimatterTable columns={columns} records={[]} emptyContent="No files found" />

Styling

Import the library stylesheet once in your app entry point or root layout:

import "bimatter-react-table/style.css";

BimatterTable exposes design tokens through CSS variables on the root .bimatter-table class.

.my-table {
    --bimatter-table-hover-bg: #edf2ff;
    --bimatter-table-selected-bg: #e7f5ff;
    --bimatter-table-border: #dee2e6;
    --bimatter-table-radius: 8px;
}
<BimatterTable className="my-table" columns={columns} records={records} />

Common CSS variables:

| Variable | Description | | ------------------------------------- | ------------------------------ | | --bimatter-table-bg | Table background. | | --bimatter-table-text | Main text color. | | --bimatter-table-border | Border color. | | --bimatter-table-header-bg | Header background. | | --bimatter-table-hover-bg | Row hover background. | | --bimatter-table-selected-bg | Selected row background. | | --bimatter-table-selected-hover-bg | Selected row hover background. | | --bimatter-table-resize-hover-color | Resize handle color on hover. | | --bimatter-table-sort-active-color | Active sort icon color. | | --bimatter-table-cell-padding-y | Vertical cell padding. | | --bimatter-table-cell-padding-x | Horizontal cell padding. | | --bimatter-table-font-size | Table font size. |

Public Exports

import {
    BimatterTable,
    BimatterTableDefault,
    type BimatterTableAlign,
    type BimatterTableCellRenderData,
    type BimatterTableColumn,
    type BimatterTableColumnAccessor,
    type BimatterTableColumnRenderData,
    type BimatterTableColumnType,
    type BimatterTableGroupRowClickHandler,
    type BimatterTableGroupRowData,
    type BimatterTableGroupLevel,
    type BimatterTableProps,
    type BimatterTableRowEventData,
    type BimatterTableRowEventHandler,
    type BimatterTableRowHandler,
    type BimatterTableRowId,
    type BimatterTableRowRenderData,
    type BimatterTableRowsDropHandler,
    type BimatterTableSelectionChangeData,
    type BimatterTableSortDirection,
    type BimatterTableSortState,
    type BimatterTableSortViewState,
    type BimatterTableToggleCollapseCallback,
    type BimatterTableToggleCollapseHandler,
} from "bimatter-react-table";