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

@waysnx/ui-grid-builder

v0.2.1

Published

Data grid component from WaysNX - sortable, filterable, paginated grid with column types and actions

Readme

@waysnx/ui-grid-builder

Data grid component from WaysNX — sortable, filterable, paginated grid with column types, row actions, and column visibility toggle. Built on TanStack Table (headless).

Table of Contents

Installation

npm install @waysnx/ui-grid-builder @tanstack/react-table

Import CSS

import '@waysnx/ui-grid-builder/dist/index.css';

Two Ways to Use

  1. Manual columns — define GridColumn[] directly in code (full control)
  2. Schema-driven — pass a JSON Schema to schemaToGridConfig() and get columns + settings auto-generated (zero-config, works with the WaysNX Grid Builder UI)

Both approaches use the same <Grid> component. Schema-driven just automates the column/settings creation.

Quick Start

import { Grid } from '@waysnx/ui-grid-builder';
import '@waysnx/ui-grid-builder/dist/index.css';

const columns = [
  { key: 'name', title: 'Name', type: 'text' },
  { key: 'email', title: 'Email', type: 'email' },
  { key: 'salary', title: 'Salary', type: 'currency', currencySymbol: '$', decimals: 0, align: 'right' },
  { key: 'joinDate', title: 'Join Date', type: 'date', dateFormat: 'dd/MM/yyyy' },
  { key: 'status', title: 'Status', type: 'badge',
    badgeMap: {
      active:   { label: 'Active',   color: '#166534', bg: '#dcfce7' },
      inactive: { label: 'Inactive', color: '#991b1b', bg: '#fee2e2' },
    }
  },
];

const actions = [
  { label: 'Edit', icon: '✏️', variant: 'primary', onClick: (row) => console.log('Edit', row) },
  { label: 'Delete', icon: '🗑️', variant: 'destructive', onClick: (row) => console.log('Delete', row) },
];

<Grid
  title="Employees"
  data={employees}
  columns={columns}
  pageSize={10}
  actions={actions}
  showColumnToggle
  showGlobalFilter
/>

Props

| Prop | Type | Default | Description | |---|---|---|---| | title | string | - | Grid title + total record count in toolbar | | data | Record<string, any>[] | required | Array of data objects | | columns | GridColumn[] | required | Column definitions | | pageSize | number | 10 | Initial number of rows shown per page | | pageSizeOptions | number[] | [5,10,25,50] | Options in the "Rows per page" dropdown. Ensure pageSize is included in this array to avoid a mismatch |

pageSize & pageSizeOptions examples:

// Default
<Grid pageSize={10} pageSizeOptions={[5, 10, 25, 50]} />

// Small datasets
<Grid pageSize={5} pageSizeOptions={[5, 10, 15, 20]} />

// Large datasets
<Grid pageSize={25} pageSizeOptions={[10, 25, 50, 100]} />

// Custom
<Grid pageSize={20} pageSizeOptions={[10, 20, 50, 100, 200]} />

Note: pageSize should always be one of the values in pageSizeOptions. If pageSize={50} and pageSizeOptions is not provided, it works correctly since 50 is in the default [5, 10, 25, 50]. If pageSize={100} without pageSizeOptions, the dropdown won't show 100 as selected — pass pageSizeOptions={[25, 50, 100]} to fix. | actions | GridAction[] | - | Row action buttons | | loading | boolean | false | Show loading skeleton | | emptyMessage | string | 'No records found' | Empty state message | | showColumnToggle | boolean | true | Column visibility toggle | | showColumnFilter | boolean | true | Show filter icons in column headers. Set false to hide all filters globally | | showGlobalFilter | boolean | false | Show global search box in toolbar that filters across all columns | | actionsAsMenu | boolean | true | Show actions as a kebab dropdown instead of inline buttons | | showRowSelection | boolean | false | Show checkbox/radio column for row selection | | selectionMode | 'checkbox' \| 'radio' | 'checkbox' | Multi-select (checkbox) or single-select (radio) | | selectionActions | GridAction[] | - | Actions shown in selection bar when rows are selected | | onSelectionChange | (rows) => void | - | Called when selection changes with selected row data | | toolbarActions | ReactNode | - | Extra toolbar buttons (Add, Download, etc.) | | onRowClick | (row) => void | - | Row click handler | | serverSide | boolean | false | Enable server-side pagination | | totalCount | number | - | Total records across all pages (required when serverSide is true) | | onPageFetch | (params) => void | - | Called on page/size change with { pageIndex, pageSize } |

Column Definition

interface GridColumn {
  key: string;           // matches data field
  title: string;         // header label
  type?: GridColumnType; // text | number | currency | percentage | email | date | boolean | badge | image | custom
  render?: (value, row) => ReactNode; // custom renderer
  width?: string;        // e.g. '150px'
  align?: 'left' | 'center' | 'right'; // default: 'left'
  sortable?: boolean;    // default: true
  filterable?: boolean;  // default: true — set false to hide filter icon for this column
  visible?: boolean;     // default: true
  dateFormat?: string;   // e.g. 'dd/MM/yyyy' (for date type)
  currencySymbol?: string;           // e.g. '$', '€' (for currency type)
  currencyPosition?: 'start' | 'end'; // default: 'start'
  decimals?: number;     // decimal places (currency default: 2, percentage default: 1)
  badgeMap?: Record<string, { label?: string; color: string; bg: string }>;
  // Maps cell value → { label, text color, background color }
  // Example:
  // badgeMap: {
  //   active:   { label: 'Active',   color: '#166534', bg: '#dcfce7' },
  //   inactive: { label: 'Inactive', color: '#991b1b', bg: '#fee2e2' },
  //   pending:  { label: 'Pending',  color: '#92400e', bg: '#fef3c7' },
  // }
}

Action Definition

interface GridAction {
  label: string;         // button text (pass '' for icon-only)
  icon?: ReactNode;      // icon element (emoji, SVG, etc.)
  variant?: 'primary' | 'secondary' | 'destructive' | 'ghost';
  onClick: (row: Record<string, any>) => void;
  // Note: for selectionActions, onClick receives the array of selected rows
  hidden?: (row: Record<string, any>) => boolean; // conditionally hide action per row
}

Icon-only actions

const actions = [
  { label: '', icon: '✏️', variant: 'primary', onClick: (row) => edit(row) },
  { label: '', icon: '🗑️', variant: 'destructive', onClick: (row) => del(row),
    hidden: (row) => !row.active },
  { label: '', icon: '👁️', onClick: (row) => view(row) },
];

Using icon libraries

Since icon accepts any ReactNode, you can use any icon library:

// FontAwesome
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faPen, faTrash, faEye } from '@fortawesome/free-solid-svg-icons';

const actions = [
  { label: 'Edit', icon: <FontAwesomeIcon icon={faPen} />, variant: 'primary', onClick: ... },
  { label: 'Delete', icon: <FontAwesomeIcon icon={faTrash} />, variant: 'destructive', onClick: ... },
];

// Lucide
import { Pencil, Trash2 } from 'lucide-react';
const actions = [
  { label: 'Edit', icon: <Pencil size={14} />, onClick: ... },
];

// Plain SVG or emoji also work
{ label: 'Edit', icon: '✏️', onClick: ... }

Column Types

| Type | Renders as | |---|---| | text | Plain string | | number | Locale-formatted number | | currency | Formatted with symbol e.g. $95,000 (use currencySymbol, currencyPosition, decimals) | | percentage | Value with % e.g. 3.5% (use decimals for decimal places) | | email | Clickable mailto link (invalid email values render as plain text for security) | | date | Formatted date (use dateFormat) |

Supported dateFormat tokens:

| Token | Description | Example | |---|---|---| | yyyy | 4-digit year | 2026 | | yy | 2-digit year | 26 | | MMMM | Full month name | March | | MMM | Short month name | Mar | | MM | 2-digit month (01–12) | 03 | | M | Month without padding | 3 | | EEEE | Full day name | Tuesday | | EEE | Short day name | Tue | | dd | 2-digit day (01–31) | 31 | | d | Day without padding | 5 | | HH | 24h hours padded (00–23) | 14 | | hh | 12h hours padded (01–12) | 02 | | h | 12h hours unpadded | 2 | | mm | Minutes padded (00–59) | 30 | | ss | Seconds padded (00–59) | 45 | | aa / a | AM/PM | PM |

Common formats:

dateFormat: 'dd/MM/yyyy'           // → 31/03/2026
dateFormat: 'MM/dd/yyyy'           // → 03/31/2026  (US)
dateFormat: 'yyyy-MM-dd'           // → 2026-03-31  (ISO)
dateFormat: 'dd MMM yyyy'          // → 31 Mar 2026
dateFormat: 'dd MMMM yyyy'         // → 31 March 2026
dateFormat: 'EEE, dd MMM yyyy'     // → Tue, 31 Mar 2026
dateFormat: 'EEEE, MMMM d, yyyy'   // → Tuesday, March 31, 2026
dateFormat: 'dd/MM/yy'             // → 31/03/26  (2-digit year)
dateFormat: 'dd/MM/yyyy HH:mm'     // → 31/03/2026 14:30
dateFormat: 'dd/MM/yyyy h:mm aa'   // → 31/03/2026 2:30 PM
dateFormat: 'dd/MM/yyyy HH:mm:ss'  // → 31/03/2026 14:30:45

If dateFormat is not specified, falls back to toLocaleDateString(). | boolean | ✓ (green) / ✗ (red) | | image | 36×36 thumbnail | | custom | Use render function | | badge | Colored pill badge (use badgeMap to map values to colors) |

Filtering

Each filterable column shows a icon in the header. Clicking it opens a dropdown with:

  • Operator — Contains, Equals, Starts with, Ends with
  • Input — filter value
  • Clear button

Global control

<Grid showColumnFilter={false} ... />  // hide all filter icons

Per-column control

const columns = [
  { key: 'name', title: 'Name', type: 'text' },           // filterable (default)
  { key: 'avatar', title: 'Photo', type: 'image',
    filterable: false },                                    // no filter icon
  { key: 'active', title: 'Active', type: 'boolean',
    filterable: false },                                    // no filter icon
];

Both can be combined — showColumnFilter={false} overrides all, filterable: false on a column hides just that column's filter.

Global search (all columns)

<Grid showGlobalFilter ... />

Shows a search box in the toolbar that filters across all visible columns simultaneously. Default is false.

Row Selection

Enable checkbox or radio selection with showRowSelection. Selected rows appear highlighted and a selection bar shows above the grid.

Checkbox (multi-select)

<Grid
  showRowSelection
  selectionMode="checkbox"  // default
  selectionActions={[
    {
      label: 'Export',
      icon: '📤',
      onClick: (selectedRows) => {
        // selectedRows is the array of all selected row data objects
        console.log(selectedRows);
      }
    },
    { label: 'Delete', icon: '🗑️', variant: 'destructive',
      onClick: (selectedRows) => deleteRows(selectedRows.map(r => r.id)) },
  ]}
  onSelectionChange={(rows) => setSelected(rows)}
/>

Radio (single-select)

<Grid
  showRowSelection
  selectionMode="radio"
  onSelectionChange={(rows) => setSelected(rows[0])}
/>

Server-Side Pagination

By default, pagination is client-side (all data loaded upfront). Enable serverSide to load only the current page's data from your API.

import { Grid } from '@waysnx/ui-grid-builder';
import { useState, useEffect } from 'react';

function EmployeeGrid() {
  const [data, setData] = useState([]);
  const [totalCount, setTotalCount] = useState(0);
  const [loading, setLoading] = useState(false);

  const fetchPage = ({ pageIndex, pageSize }) => {
    setLoading(true);
    fetch(`/api/employees?page=${pageIndex + 1}&limit=${pageSize}`)
      .then(r => r.json())
      .then(res => {
        setData(res.data);           // current page rows only
        setTotalCount(res.total);    // total records across all pages
        setLoading(false);
      });
  };

  useEffect(() => { fetchPage({ pageIndex: 0, pageSize: 10 }); }, []);

  return (
    <Grid
      serverSide
      data={data}
      totalCount={totalCount}
      onPageFetch={fetchPage}
      loading={loading}
      columns={columns}
      pageSize={10}
      title="Employees"
    />
  );
}

| Prop | Type | Default | Description | |---|---|---|---| | serverSide | boolean | false | Enable server-side pagination | | totalCount | number | — | Total records across all pages (required when serverSide is true) | | onPageFetch | (params) => void | — | Called on page/size change with { pageIndex, pageSize } |

How it works:

  • Grid renders whatever data is passed (assumes it's the current page)
  • Pagination uses totalCount for page math ("1–10 of 500")
  • On page change or page size change, onPageFetch is called — your app fetches the new page
  • Sorting and filtering still work client-side on the loaded page
  • No HTTP requests are made by the library (SSRF-safe)

For schema-driven grids, use x-grid-pagination.serverSide:

{
  "x-grid-pagination": {
    "pageSize": 10,
    "serverSide": true
  }
}

Schema-Driven Grid

Generate grid columns and settings from a JSON Schema using schemaToGridConfig. This is the runtime consumer for configurations produced by the WaysNX Grid Builder UI or any OpenAPI-compatible schema.

Basic Usage

import { Grid, schemaToGridConfig } from '@waysnx/ui-grid-builder';
import type { GridSchema } from '@waysnx/ui-grid-builder';

const schema: GridSchema = {
  type: 'object',
  'x-grid-settings': {
    title: 'Employees',
  },
  'x-grid-pagination': {
    pageSize: 10,
  },
  'x-grid-filters': {
    showGlobalFilter: true,
  },
  properties: {
    name: { type: 'string', title: 'Name' },
    email: { type: 'string', format: 'email', title: 'Email' },
    salary: { type: 'number', title: 'Salary', 'x-currency-symbol': '$' },
    joinDate: { type: 'string', format: 'date', title: 'Join Date', 'x-date-format': 'dd/MM/yyyy' },
    active: { type: 'boolean', title: 'Active' },
  },
};

const { columns, gridProps, actionDefs } = schemaToGridConfig(schema);

<Grid columns={columns} data={data} {...gridProps} />

Auto Type Inference

Column types are inferred from JSON Schema type + format:

| Schema type | Schema format | Grid column type | |---|---|---| | string | — | text | | string | email | email | | string | date / date-time | date | | number / integer | — | number | | number + x-currency-symbol | — | currency | | boolean | — | boolean |

Override with x-grid-type for types that can't be inferred: badge, image, percentage, custom.

Schema Property Extensions (x-grid-*)

| Extension | Type | Description | |---|---|---| | x-grid-type | GridColumnType | Force column type (overrides auto-inference) | | x-grid-width | string | Column width e.g. '60px' | | x-grid-align | 'left' \| 'center' \| 'right' | Text alignment (auto: right for number/currency) | | x-grid-sortable | boolean | Enable/disable sorting | | x-grid-filterable | boolean | Enable/disable column filter | | x-grid-visible | boolean | Show/hide column by default | | x-grid-decimals | number | Decimal places for currency/percentage | | x-grid-badge-map | Record<string, {label, color, bg}> | Badge color mapping | | x-currency-symbol | string | Currency symbol (also triggers currency type) | | x-currency-position | 'start' \| 'end' | Currency symbol position | | x-date-format | string | Date format tokens |

Grid-Level Settings (Grouped)

Settings are organized into logical groups. Flat x-grid-* properties are still supported for backward compatibility but grouped format is recommended.

x-grid-settings — General

| Key | Type | Default | Description | |---|---|---|---| | title | string | — | Grid title | | emptyMessage | string | 'No records found' | Empty state text |

x-grid-pagination — Pagination

| Key | Type | Default | Description | |---|---|---|---| | pageSize | number | 10 | Rows per page | | pageSizeOptions | number[] | [5,10,25,50] | Page size dropdown options | | serverSide | boolean | false | Enable server-side pagination (future) |

x-grid-filters — Filtering

| Key | Type | Default | Description | |---|---|---|---| | showGlobalFilter | boolean | false | Show global search | | showColumnFilter | boolean | true | Show per-column filters |

x-grid-columns — Column Controls

| Key | Type | Default | Description | |---|---|---|---| | showColumnToggle | boolean | true | Show column visibility toggle |

x-grid-actions — Row Actions

| Key | Type | Default | Description | |---|---|---|---| | actionsAsMenu | boolean | true | Kebab menu vs inline buttons | | items | GridActionDef[] | [] | Action definitions (name, label, icon, variant) |

x-grid-selection — Row Selection

| Key | Type | Default | Description | |---|---|---|---| | enabled | boolean | false | Enable row selection | | mode | 'checkbox' \| 'radio' | 'checkbox' | Selection mode |

Example:

{
  "type": "object",
  "x-grid-settings": { "title": "Employees" },
  "x-grid-pagination": { "pageSize": 10, "pageSizeOptions": [5, 10, 25] },
  "x-grid-filters": { "showGlobalFilter": true },
  "x-grid-columns": { "showColumnToggle": true },
  "x-grid-actions": {
    "actionsAsMenu": true,
    "items": [
      { "name": "edit", "label": "Edit", "icon": "✏️", "variant": "primary" },
      { "name": "delete", "label": "Delete", "icon": "🗑️", "variant": "destructive" }
    ]
  },
  "x-grid-selection": { "enabled": true, "mode": "checkbox" },
  "properties": { ... }
}

Action Definitions

Actions are defined in x-grid-actions.items. The consuming app maps them to handlers:

{
  "x-grid-actions": {
    "actionsAsMenu": true,
    "items": [
      { "name": "edit", "label": "Edit", "icon": "✏️", "variant": "primary" },
      { "name": "delete", "label": "Delete", "icon": "🗑️", "variant": "destructive" }
    ]
  }
}
const { columns, gridProps, actionDefs } = schemaToGridConfig(schema);

// Map action definitions to handlers
const actions = actionDefs.map(def => ({
  ...def,
  onClick: (row) => {
    switch (def.name) {
      case 'edit': openEditModal(row); break;
      case 'delete': handleDelete(row.id); break;
    }
  },
}));

<Grid columns={columns} data={data} {...gridProps} actions={actions} />

Return Value

schemaToGridConfig returns:

interface GridConfig {
  columns: GridColumn[];     // Column definitions
  gridProps: Partial<GridProps>; // Grid settings (title, pageSize, filters, etc.)
  actionDefs: GridActionDef[];  // Action metadata (name, label, icon, variant)
}

Overriding Schema Values

User props override schema values since {...gridProps} is spread before explicit props:

const { columns, gridProps } = schemaToGridConfig(schema);

<Grid
  columns={columns}
  data={data}
  {...gridProps}
  pageSize={5}  // overrides schema's x-grid-page-size
/>

Custom Renderers

The custom column type requires a render function which can't come from JSON. Override after conversion:

const { columns } = schemaToGridConfig(schema);

const nameCol = columns.find(c => c.key === 'name');
if (nameCol) {
  nameCol.render = (value, row) => (
    <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
      <img src={row.avatar} style={{ width: 28, borderRadius: '50%' }} />
      <span>{value}</span>
    </div>
  );
}

Complete Example

import { Grid, schemaToGridConfig } from '@waysnx/ui-grid-builder';
import type { GridSchema } from '@waysnx/ui-grid-builder';
import { useState, useEffect } from 'react';

const schema: GridSchema = {
  type: 'object',
  'x-grid-settings': { title: 'Employees' },
  'x-grid-pagination': { pageSize: 10, pageSizeOptions: [5, 10, 25] },
  'x-grid-filters': { showGlobalFilter: true },
  'x-grid-columns': { showColumnToggle: true },
  'x-grid-actions': {
    actionsAsMenu: true,
    items: [
      { name: 'edit', label: 'Edit', icon: '✏️', variant: 'primary' },
      { name: 'delete', label: 'Delete', icon: '🗑️', variant: 'destructive' },
    ],
  },
  'x-grid-selection': { enabled: true, mode: 'checkbox' },
  properties: {
    avatar: { type: 'string', title: 'Photo', 'x-grid-type': 'image', 'x-grid-width': '60px', 'x-grid-sortable': false, 'x-grid-filterable': false },
    name: { type: 'string', title: 'Name' },
    email: { type: 'string', format: 'email', title: 'Email' },
    salary: { type: 'number', title: 'Salary', 'x-currency-symbol': '$', 'x-grid-decimals': 0, 'x-grid-align': 'right' },
    joinDate: { type: 'string', format: 'date', title: 'Join Date', 'x-date-format': 'dd/MM/yyyy' },
    status: { type: 'string', title: 'Status', 'x-grid-type': 'badge', 'x-grid-badge-map': {
      active: { label: 'Active', color: '#166534', bg: '#dcfce7' },
      inactive: { label: 'Inactive', color: '#991b1b', bg: '#fee2e2' },
    }},
    active: { type: 'boolean', title: 'Active', 'x-grid-width': '80px' },
  },
};

function EmployeeGrid() {
  const [data, setData] = useState([]);
  const { columns, gridProps, actionDefs } = schemaToGridConfig(schema);

  const actions = actionDefs.map(def => ({
    ...def,
    onClick: (row) => alert(`${def.name}: ${row.name}`),
  }));

  useEffect(() => {
    fetch('/api/employees').then(r => r.json()).then(setData);
  }, []);

  return (
    <Grid
      columns={columns}
      data={data}
      {...gridProps}
      actions={actions}
      toolbarActions={<button onClick={() => alert('Add')}>+ Add</button>}
      onRowClick={(row) => console.log('Clicked:', row)}
    />
  );
}

TypeScript Types

import type {
  GridSchema,
  GridSchemaProperty,
  GridActionDef,
  GridConfig,
  GridSettingsGroup,
  GridPaginationGroup,
  GridFiltersGroup,
  GridColumnsGroup,
  GridActionsGroup,
  GridSelectionGroup,
} from '@waysnx/ui-grid-builder';

Theming

Uses the same CSS variables as all WaysNX libraries:

:root {
  --wx-color-primary: #f19924;
  --wx-color-text: #1e293b;
  --wx-color-surface: #ffffff;
  --wx-color-border: #e2e8f0;
}

These are the same variables used across all WaysNX libraries. See the Theming Guide for the full list.

Security

This library is designed with security as a top priority:

  • No XSS vulnerabilities — All cell values are rendered via React JSX (auto-escaped). No dangerouslySetInnerHTML or direct DOM manipulation. Badge labels, formatted values, and custom renderers all go through React's safe rendering pipeline.
  • No SSRF vulnerabilities — The library makes zero HTTP requests. Data is always passed via the data prop. For schema-driven grids, the consuming app controls all API calls.
  • No Code Injection — No eval(), new Function(), or dynamic code execution. Filter operators use string comparison, not executed code. Schema extensions are read as plain data, never evaluated.

Peer Dependencies

  • react >= 18
  • react-dom >= 18
  • @tanstack/react-table ^8.0.0

License

MIT © WaysNX Technologies