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

apex-grid

v3.2.0

Published

Web component data grid following open-wc recommendations

Readme

Apex Grid

Node.js CI Coverage Status npm

A Lit-based, framework-agnostic web component data grid. Ships as a single custom element <apex-grid> with a rich, opt-in feature set and full TypeScript types.

Features

  • Row virtualization via @lit-labs/virtualizer — only ~20 rows in the DOM at any time, regardless of dataset size.
  • Sorting — single or multi-column, tri-state (asc / desc / none), per-column comparers.
  • Filtering — per-column filter chips with string / number / boolean / date operands, plus a quick-filter (global search) input.
  • Pagination — local slicing or remote mode with a pageChanging / pageChanged event pair; built-in <apex-grid-paginator>.
  • Column pinning — pin to start or end; visual reordering only, source columns array is preserved.
  • Column reordering — drag-and-drop with per-column opt-out, constrained to the column's pinning group.
  • Column resizing — pointer-driven, with a min-width safeguard.
  • Inline editing — cell or row mode, click or double-click trigger, per-column opt-in.
  • Row selection — single or multiple, optional checkbox column, full programmatic API.
  • Row expansion (master-detail) — opt-in chevron column with a detailTemplate.
  • Tree data (nested rows) — AG Grid–style getDataPath pattern over a flat array.
  • CSV export — programmatic method plus an optional toolbar dropdown. (Excel/XLSX export is in apex-grid-enterprise.)
  • Toolbar — opt-in <apex-grid-toolbar> with debounced quick filter and export menu.
  • Templating — slot-based templates for cells, headers, editors, and detail panels.
  • Theming — styled out-of-the-box; fully customizable through --ag-* CSS custom properties (no theme import or build step). Auto-matches an igniteui-webcomponents host app when one is present.
  • Accessibility — WCAG 2.2 AA semantics (role="grid" / role="treegrid", aria-rowcount, aria-colcount, focus + keyboard navigation).
  • Provenance-signed npm releases with OIDC trusted publishing.

Quick Start

One-call setup

import { setup } from 'apex-grid';

setup();

That single call registers <apex-grid> and adopts a default host stylesheet (height: 100%; min-height: 240px). The grid is styled out-of-the-box — no theme CSS import is required.

Render the grid

import { html, render } from 'lit';
import 'apex-grid/define';
import type { ColumnConfiguration } from 'apex-grid';

type User = { id: number; name: string; age: number; subscribed: boolean };

const data: User[] = [
  { id: 1, name: 'Ada Lovelace', age: 36, subscribed: true },
  { id: 2, name: 'Carl Sagan',   age: 62, subscribed: false },
  { id: 3, name: 'Grace Hopper', age: 85, subscribed: true },
];

const columns: ColumnConfiguration<User>[] = [
  { key: 'id',         type: 'number',  headerText: 'ID',         width: '80px',  sort: true, filter: true },
  { key: 'name',       type: 'string',  headerText: 'Name',       width: '240px', sort: true, filter: true },
  { key: 'age',        type: 'number',  headerText: 'Age',        width: '100px', sort: true, filter: true },
  { key: 'subscribed', type: 'boolean', headerText: 'Subscribed', width: '140px', sort: true, filter: true },
];

render(
  html`<apex-grid .data=${data} .columns=${columns}></apex-grid>`,
  document.getElementById('app')!,
);
<style>
  apex-grid { height: 480px; }
</style>
<div id="app"></div>

Manual setup (four steps)

If you'd rather not use setup(), this is what it does under the hood. Skipping any step produces a grid that "runs" but renders broken (no borders, no filter UI, or only a few collapsed rows).

1. Install

npm install apex-grid lit

igniteui-webcomponents ships as a transitive dependency — no separate install.

2. Register the custom element

import 'apex-grid/define';

Equivalent long form:

import { ApexGrid } from 'apex-grid';
ApexGrid.register();

Without this, <apex-grid> is an inert unknown element.

3. (Optional) Customize the look

The grid is styled out-of-the-box — there is no theme to import. Customize it by overriding the --ag-* CSS custom properties on apex-grid (or any ancestor); a one-line brand override cascades to every tint:

apex-grid {
  --ag-brand: #7c3aed;        /* selection, focus, accents */
  --ag-brand-strong: #6d28d9; /* hover / pressed */
  --ag-radius: 12px;          /* outer card radius */
  --ag-row-h: 40px;           /* row height */
}

See src/styles/_tokens.scss for the full token list (brand, surfaces, text, semantic state colors, typography, spacing, motion).

Grid edge / shadow. By default the grid shows a flat 1px hairline edge (no drop shadow). Control it with the --ag-grid-shadow hook — this is an opt-in override, not one of the _tokens.scss defaults:

apex-grid { --ag-grid-shadow: var(--ag-shadow-card); } /* elevated floating-card look */
apex-grid { --ag-grid-shadow: none; }                  /* remove the edge entirely */

If you embed the grid alongside igniteui-webcomponents, the brand tokens automatically re-tint from the igniteui palette (--ig-primary-500) — no configuration needed.

4. Size the host

@lit-labs/virtualizer requires a bounded height. Without one, the virtualizer collapses to its natural content height (~150px) and only a few rows ever render.

apex-grid {
  height: 480px;   /* any explicit pixel height; % works if the parent has a height */
}

[!TIP] import 'apex-grid/styles.css' ships a default rule that sets height: 100% with a min-height: 240px fallback.

[!IMPORTANT] Do not set display on <apex-grid>. The component declares :host { display: grid } internally for its track layout (header / filter / body). Any consumer rule that sets display (including block, flex, inline-block) collapses the grid. If you accidentally do this, the grid emits a one-shot console.warn at startup pointing here.

What success looks like

With the element registered and the host sized, you should see:

  • Visible borders between rows and columns.
  • Sort arrows (↕) next to each header when sort: true.
  • A filter row below the headers with a "Filter" chip per column when filter: true.
  • Hover state on rows.
  • Smooth scrolling — DevTools shows only ~20 <apex-grid-row> elements at any time.

Troubleshooting

| What you see | Likely cause | |---|---| | Want a different look / brand color | Step 3 — override the --ag-* CSS variables | | Only ~3 rows visible regardless of data size | Step 4 — no bounded height, or consumer CSS sets display on <apex-grid> (check console for the warning) | | <apex-grid> blank tag in DOM | Step 2 — element not registered | | Columns shown as literal [object Object] | columns= used as an attribute — must be a property (.columns=${...} in Lit, [columns]= in Angular, :columns.prop= in Vue, el.columns = ... in vanilla JS) |


Features in depth

Each feature below is fully opt-in — you only pay for what you turn on. Snippets assume const grid = document.querySelector('apex-grid')!.

Sorting

const columns = [
  { key: 'name', sort: true },                            // UI sort + tri-state
  { key: 'age',  sort: { direction: 'desc' } },           // initial state
];

grid.sortConfiguration = { multiple: true, triState: true };
grid.sort({ key: 'age', direction: 'asc' });
grid.clearSort();                                          // or grid.clearSort('age')

When multiple is enabled, a plain header click sorts by that column alone; hold Ctrl/Cmd and click to append additional columns as lower-priority sort keys. Events: sorting (cancellable), sorted.

Filtering

const columns = [
  { key: 'name', filter: true },                          // UI filter chip
  { key: 'age',  filter: true, type: 'number' },          // operands by type
];

import { StringOperands } from 'apex-grid';
grid.filter({ key: 'name', condition: StringOperands.contains, searchTerm: 'Ada' });
grid.clearFilter();

Operand classes: StringOperands, NumberOperands, BooleanOperands. Events: filtering (cancellable), filtered.

Quick filter (global search)

grid.showQuickFilter = true;       // renders the toolbar input
grid.quickFilter = 'ada';          // or: await grid.setQuickFilter('ada')

Custom matcher via dataPipelineConfiguration.quickFilter. Events: quickFilterChanging (cancellable), quickFilterChanged. Attribute: show-quick-filter, quick-filter.

Pagination

grid.pagination = {
  enabled: true,
  pageSize: 25,
  pageSizeOptions: [10, 25, 50, 100],
};

await grid.gotoPage(2);
await grid.setPageSize(50);
grid.nextPage(); grid.previousPage(); grid.firstPage(); grid.lastPage();

Remote mode:

grid.pagination = { enabled: true, mode: 'remote', pageSize: 25, totalItems: 1280 };
grid.addEventListener('pageChanged', async (e) => {
  grid.data = await fetchPage(e.detail.page, e.detail.pageSize);
});

Properties: page, pageSize, pageCount, totalItems, pageItems. Events: pageChanging (cancellable), pageChanged.

Column pinning

const columns = [
  { key: 'id',   pinned: 'start' },
  { key: 'name' },
  { key: 'actions', pinned: 'end' },
];

await grid.pinColumn('name', 'start');
await grid.unpinColumn('name');                 // or pinColumn('name', null)

The source columns array is not reordered — only the visual render order changes. Read grid.displayColumns for the render order. Events: columnPinning (cancellable), columnPinned.

Column reordering

<apex-grid column-reordering></apex-grid>

Or programmatic:

await grid.moveColumn('email', 'name', 'after');

Per-column opt-out: { key: 'id', reorderable: false }. Reordering is constrained to the column's own pinning group (start / unpinned / end). Events: columnMoving (cancellable), columnMoved. Attribute: column-reordering.

Inline editing

const columns = [
  { key: 'name', editable: true },
  { key: 'age',  editable: true, type: 'number' },
];

grid.editing = { enabled: true, mode: 'cell', trigger: 'doubleClick' };

await grid.editCell(0, 'name');
await grid.commitEdit();
grid.cancelEdit();

mode: 'row' puts all editable cells in the row into edit together. Properties: editingCell, editingRow. Events: cellValueChanging (cancellable), cellValueChanged, plus rowEditStarted / rowEditEnded in row mode.

Row selection

grid.selection = { enabled: true, mode: 'multiple', showCheckboxColumn: true };

await grid.selectRow(data[0]);
await grid.toggleRowSelection(data[1]);
await grid.selectAllRows();
await grid.clearSelection();
grid.selectedRows;                  // snapshot
grid.selectedRows = [data[2]];      // replace selection (goes through `rowSelecting`)

Events: rowSelecting (cancellable), rowSelected.

Row expansion (master-detail)

grid.expansion = {
  enabled: true,
  detailTemplate: ({ data }) => html`<order-summary .order=${data}></order-summary>`,
  isExpandable: (row) => row.hasDetails,
};

await grid.expandRow(data[0]);
await grid.toggleRowExpansion(data[0]);
await grid.expandAllRows();
await grid.collapseAllRows();
grid.expandedRows;                  // snapshot

Events: rowExpanding (cancellable), rowExpanded.

Tree data (nested rows)

The data array stays flat. The grid derives the hierarchy from a getDataPath(row) callback that returns the path from root to that row — AG Grid's "tree data" pattern.

type Person = { id: number; name: string; title: string; path: string[] };

const data: Person[] = [
  { id: 1, name: 'Adrian',  title: 'CEO',     path: ['Adrian'] },
  { id: 2, name: 'Bryan',   title: 'VP Eng',  path: ['Adrian', 'Bryan'] },
  { id: 3, name: 'Cara',    title: 'Manager', path: ['Adrian', 'Bryan', 'Cara'] },
];

grid.tree = {
  enabled: true,
  getDataPath: (row) => row.path,
  defaultExpanded: 1,              // boolean | number — depth to expand
  groupColumnKey: 'name',          // which column shows the chevron + indent
  childIndent: 20,                 // px per depth level
};

await grid.toggleTreeRow(data[0]);
await grid.expandAllTreeRows();

Methods: toggleTreeRow, expandTreeRow, collapseTreeRow, expandAllTreeRows, collapseAllTreeRows, isTreeRowExpanded. Events: treeRowExpanding (cancellable), treeRowExpanded. When tree mode is active, the host element advertises role="treegrid".

CSV export

Programmatic:

grid.exportToCSV();                                            // downloads data.csv
grid.exportToCSV({ filename: 'users', source: 'selected' });
const text = grid.exportToCSV({ filename: '' });               // no download, returns the string

source can be 'view' (default — post-filter/post-sort), 'page', 'selected', or 'all'. Per-column opt-out: { key: 'secret', exportable: false }.

XLSX (Excel) export moved to apex-grid-enterprise in v3. <apex-grid-enterprise> adds grid.exportToXLSX(...) and an "Export XLSX" entry to this same toolbar menu. CSV stays free.

Toolbar dropdown:

<apex-grid show-export></apex-grid>

Renders a download icon in the toolbar's trailing actions area; the menu has an "Export CSV" entry (the enterprise grid adds "Export XLSX"). Toolbar exportFilename overrides the default data filename. Attribute: show-export.

Toolbar

Rendered automatically above the header row when at least one of show-quick-filter or show-export is on. CSS parts:

| Part | What | |---|---| | toolbar | Root container | | toolbar-search | Quick-filter input wrapper | | search-field | The bordered input field | | search-icon, search-input | Leading icon, input element | | toolbar-actions | Trailing actions area | | export-trigger | Export menu button | | export-menu | Dropdown panel | | export-menu-item | Menu item |

Search input has a debounce attribute (default 200ms).

Theming

The grid styles itself through --ag-* CSS custom properties — override them on apex-grid (or any ancestor) to rebrand; see src/styles/_tokens.scss for the full list. When igniteui-webcomponents is present, the brand tokens auto-tint from its palette.

Style with CSS parts on the grid, paginator, and toolbar:

apex-grid::part(paginator) { background: var(--surface-2); }
apex-grid-toolbar::part(search-input) { font-family: var(--font-mono); }

API Reference

Properties

| Property | Type | Default | Notes | |---|---|---|---| | data | T[] | [] | Source records (property only) | | columns | ColumnConfiguration<T>[] | [] | Column configuration (property only) | | autoGenerate | boolean | false | Infer columns from data[0] keys. Attr auto-generate | | sortConfiguration | GridSortConfiguration | { multiple, triState } | | | dataPipelineConfiguration | DataPipelineConfiguration<T> | — | Custom sort/filter/pagination hooks | | pagination | PaginationConfiguration | — | | | quickFilter | string | '' | Attr quick-filter | | showQuickFilter | boolean | false | Attr show-quick-filter | | showExport | boolean | false | Attr show-export | | columnReordering | boolean | false | Attr column-reordering | | editing | GridEditingConfiguration | — | | | selection | GridSelectionConfiguration | — | | | expansion | GridExpansionConfiguration<T> | — | | | tree | GridTreeConfiguration<T> | — | | | sortExpressions | SortExpression<T>[] | — | Get/set | | filterExpressions | FilterExpression<T>[] | — | Get/set | | selectedRows | T[] | — | Get/set | | expandedRows | T[] | — | Get/set | | page, pageSize, pageCount, totalItems | number | — | | | pageItems | readonly T[] | — | Currently rendered slice | | dataView | readonly T[] | — | Post-filter, post-sort | | displayColumns | readonly ColumnConfiguration<T>[] | — | Render order (pinned start → unpinned → pinned end) | | editingCell | { rowIndex, columnKey } \| null | — | | | editingRow | number \| null | — | Row-mode only |

Methods

sort(expr): void
filter(expr): void
clearSort(key?): void
clearFilter(key?): void
setQuickFilter(value): Promise<boolean>

getColumn(keyOrIndex): ColumnConfiguration<T> | undefined
updateColumns(columns): void
pinColumn(key, 'start' | 'end' | null): Promise<boolean>
unpinColumn(key): Promise<boolean>
moveColumn(fromKey, toKey, 'before' | 'after'): Promise<boolean>

gotoPage(page): Promise<boolean>
setPageSize(size): Promise<boolean>
nextPage(); previousPage(); firstPage(); lastPage()

editCell(rowIndex, columnKey): Promise<boolean>
editRow(rowIndex): Promise<boolean>
commitEdit(): Promise<boolean>
cancelEdit(): void

selectRow(row); deselectRow(row); toggleRowSelection(row)
selectAllRows(); clearSelection(); isRowSelected(row)

expandRow(row); collapseRow(row); toggleRowExpansion(row)
expandAllRows(); collapseAllRows(); isRowExpanded(row)

toggleTreeRow(row); expandTreeRow(row); collapseTreeRow(row)
expandAllTreeRows(); collapseAllTreeRows(); isTreeRowExpanded(row)

exportToCSV(options?): string
exportAs(formatId, options?): void   // toolbar dispatch; 'csv' (community), 'xlsx' (enterprise)

Events

All events bubble and are composed across shadow boundaries. Names ending in -ing are cancellable.

| Event | Cancellable | Detail | |---|---|---| | sorting / sorted | yes / no | SortExpression<T>[] | | filtering / filtered | yes / no | FilterExpression<T>[] | | quickFilterChanging / quickFilterChanged | yes / no | { value, nextValue? } | | pageChanging / pageChanged | yes / no | { page, pageSize, pageCount, totalItems } | | columnPinning / columnPinned | yes / no | { key, previous, next } / { key, pinned } | | columnMoving / columnMoved | yes / no | { key, fromIndex, toKey, position } / { key, fromIndex, toIndex } | | cellValueChanging / cellValueChanged | yes / no | { row, column, value, newValue } | | rowEditStarted / rowEditEnded | no / no | row context | | rowSelecting / rowSelected | yes / no | { added, removed } | | rowExpanding / rowExpanded | yes / no | row context | | treeRowExpanding / treeRowExpanded | yes / no | row context |

Programmatic sort() / filter() calls are silent — only UI-initiated changes emit sorting / filtering.

Attributes

auto-generate, quick-filter, show-quick-filter, show-export, column-reordering.

CSS parts

| Component | Parts | |---|---| | <apex-grid-toolbar> | toolbar, toolbar-search, search-field, search-icon, search-input, toolbar-actions, export-trigger, export-menu, export-menu-item | | <apex-grid-paginator> | paginator, paginator-size, paginator-info, paginator-controls, paginator-page | | <apex-grid-cell> | cell, editor |


Framework integration

<apex-grid> is a standard custom element. Bind properties (not attributes) for data / columns:

| Framework | Syntax | |---|---| | Lit | <apex-grid .data=${data} .columns=${columns}> | | Angular | <apex-grid [data]="data" [columns]="columns"> (use CUSTOM_ELEMENTS_SCHEMA) | | Vue | <apex-grid :data.prop="data" :columns.prop="columns"> | | React (19+) | <apex-grid data={data} columns={columns}> | | Vanilla | el.data = data; el.columns = columns; |


Local development

git clone https://github.com/apexcharts/apex-grid.git
cd apex-grid
npm install
npm start             # demo at http://localhost:5173
npm test              # web-test-runner
npm run lint
npm run build         # builds dist/ + custom-elements.json + typedoc

Releasing

Releases are automated by .github/workflows/publish.yml:

  1. Bump "version" in package.json — the single source of truth. The build injects it into dist/package.json.
  2. Commit with a message starting with release: and the same version, e.g. release: 2.0.0 or release: 2.0.0-rc.1.
  3. Push to main.

The workflow then verifies the version triple-match, runs lint + tests + build, publishes dist/ to npm with --provenance (OIDC trusted publishing — no token in secrets), and creates a vX.Y.Z git tag and GitHub Release with auto-generated notes. Pre-release versions (containing -) publish under the next dist-tag; stable versions under latest.

Any push whose head commit does not start with release: is a no-op for the workflow.

License

See LICENSE.