@get-set/gs-table
v1.0.3
Published
Get-Set Table  a beautiful, feature-complete data table / grid for vanilla JS, jQuery, prototype and React.
Downloads
367
Maintainers
Readme
@get-set/gs-table
A dependency-free, fully accessible (WAI-ARIA grid) data table / grid
available in four usage modes from one codebase:
- Native / vanilla JS — a
window.GSTable(selector | element, params)factory plus awindow.GSTableConfigueinstance registry. - jQuery —
$(selector).GSTable(params)(auto-registered by the bundle). - Prototype —
element.GSTable(params)onHTMLElement.prototype(auto-registered by the bundle). - React — a
<GSTable />forwardRefcomponent with a typed imperativeGSTableHandleand a React-only scoped-stylesgsxprop.
All four surfaces drive one shared core (actions/, constants/, helpers/,
types/), so behaviour is identical — every option works in every target
(except the few tagged React-only below).
Features
- Multi-column sorting — click cycles
none → asc → desc → none; enablemultiSortto stack sorts with visible priority badges. CustomsortComparatorper column. Beautiful animated sort-arrow. - Per-column filters + global search —
text,select(options inferred from the data or supplied), andnumberfilters with>,<,>=,<=,=operators. Optional debounce. - Pagination modes — client-side
pagination(with a page-size selector),load-more, orinfinitescroll. - Row selection —
single(radio) ormultiple(checkbox) with a header select-all,isRowSelectablepredicate, andgetSelected()/getSelectedRows(). - Expandable rows — per-row detail renderer,
singleExpandmode, animated chevron + reveal. - Sticky header + pinnable columns — pin a column to the
start/endedge withpinned: 'start' | 'end'(friendly alias of the legacysticky); pinned columns stay fixed while the rest scroll horizontally, with correct cumulative offsets (including the checkbox / expander lead columns), an edge shadow, and RTL support. Scrollable body withmaxHeight. - Responsive horizontal scroll — a too-wide table scrolls horizontally
inside its own box (
overflow-x: auto, touch-momentum on iOS) instead of breaking the page layout; columns keep a readable min-width. - Resizable columns (drag the divider) and reorderable columns (drag & drop headers).
- Variants —
striped/bordered/hover/compact(stackable), plus azebrashorthand. - Loading skeleton rows, a polished empty state, and per-row / per-cell
relayout animations (all respect
prefers-reduced-motion). - Custom cell + header renderers,
rowKey,onRowClick, density (comfortable/cozy/compact) and size (sm/md/lg). - Row context menu — an optional, self-contained right-click menu per row
(
rowContextMenuarray or per-row builder,onRowContextMenu) with submenus, separators, danger + disabled items, full keyboard + ARIA support. No extra dependency. - Light / dark / auto theming + custom
accentColor, RTL, per-partcustomClassoverrides, ARIAgridroles + full keyboard support, and a React-only scoped-stylesgsxprop. - Rich callbacks —
onSort,onFilter,onPageChange,onSelectionChange,onExpandChange,onRowClick,onColumnResize,onColumnReorder.
Installation
npm i @get-set/gs-tableCompatibility
| Target | Requirement |
|---|---|
| React (the <GSTable> component) | React 16.8+ (Hooks + forwardRef), and 17 / 18 / 19. React is an optional peer dependency — you only need it for the component. |
| Native / vanilla (window.GSTable) | No framework. Any modern evergreen browser. The optional jQuery ($(...).GSTable(...)) and HTMLElement.prototype.GSTable(...) adapters are registered automatically by the bundle. |
| TypeScript | First-class — type declarations (.d.ts) ship in the package. |
| SSR / Next.js | Safe to import server-side. Render inside a Client Component ('use client') — the table is a browser widget. |
The peer range is
^16.8.0 || ^17.0 || ^18.0 || ^19.0, withreact/react-dommarked optional so the native build has zero dependencies.
Project layout
GSTable.ts # native entry (webpack -> dist-js/bundle.js, window global)
components/GSTable.tsx # React component (tsc -> dist/, npm entry)
components/styles/GSTableCSS.ts # CSS-in-TS injected by the React component
actions/ # shared engine: init/render/interactions/destroy/refresh
helpers/ # data pipeline, columns, layout, icons, ui helpers
constants/ # defaults + size/density scales
types/ # Params, Ref, GSTableColumn, global.d.ts
styles/GSTable.css # stylesheet for the native / jQuery / prototype builds
example.html # runnable native showcaseVANILLA (window factory)
The factory accepts a selector string OR a DOM element — the same signature the jQuery + prototype adapters call internally.
<link rel="stylesheet" href="node_modules/@get-set/gs-table/styles/GSTable.css" />
<div id="people"></div>
<script src="node_modules/@get-set/gs-table/dist-js/bundle.js"></script>
<script>
// (a) selector string
const grid = window.GSTable('#people', {
reference: 'people',
rowKey: 'id',
searchable: true,
selectable: 'multiple',
pageSize: 10,
variant: ['striped', 'hover'],
columns: [
{ key: 'id', label: '#', width: 60, align: 'right' },
{ key: 'name', sortable: true, filterable: true },
{ key: 'team', sortable: true, filterable: true, filterType: 'select' },
{ key: 'salary', align: 'right', sortable: true,
render: ({ value }) => '$' + value.toLocaleString() }
],
data: [
{ id: 1, name: 'Ada Lovelace', team: 'Platform', salary: 145000 },
{ id: 2, name: 'Alan Turing', team: 'Data', salary: 138000 }
],
onSort: (sort) => console.log('sort', sort),
onSelectionChange: (keys, rows) => console.log('selected', keys, rows)
});
// (b) a DOM element works too
const el = document.getElementById('people2');
window.GSTable(el, { rowKey: 'id', columns, data });
</script>Instance registry
Every instance is registered under its reference (auto-generated when omitted):
window.GSTable('#people', { reference: 'people', columns, data });
const grid = window.GSTableConfigue.instance('people'); // -> the Ref
grid.sortBy('salary', 'desc');
grid.setPage(2);
console.log(grid.getSelectedRows());
grid.destroy();JQUERY
Loaded automatically by the bundle when window.jQuery is present. It calls the
factory once per matched element (passing the element, not the selector).
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="node_modules/@get-set/gs-table/dist-js/bundle.js"></script>
<script>
$('#people').GSTable({ rowKey: 'id', columns, data, searchable: true });
</script>PROTOTYPE
Also registered by the bundle — call it directly on any element:
document.getElementById('people').GSTable({ rowKey: 'id', columns, data });REACT
'use client';
import React, { useRef } from 'react';
import GSTable, { GSTableHandle } from '@get-set/gs-table';
interface Person { id: number; name: string; team: string; salary: number; }
const data: Person[] = [
{ id: 1, name: 'Ada Lovelace', team: 'Platform', salary: 145000 },
{ id: 2, name: 'Alan Turing', team: 'Data', salary: 138000 }
];
export default function People() {
const ref = useRef<GSTableHandle<Person>>(null);
return (
<>
<button onClick={() => ref.current?.selectAll()}>Select all</button>
<button onClick={() => ref.current?.sort('salary', 'desc')}>Sort by pay</button>
<GSTable<Person>
ref={ref}
rowKey="id"
data={data}
searchable
selectable="multiple"
expandable
variant={['striped', 'hover']}
theme="auto"
accentColor="#7c3aed"
pageSize={10}
columns={[
{ key: 'id', label: '#', width: 60, align: 'right' },
{ key: 'name', sortable: true, filterable: true },
{ key: 'team', sortable: true, filterable: true, filterType: 'select' },
{ key: 'salary', align: 'right', sortable: true,
render: ({ value }) => <b>${Number(value).toLocaleString()}</b> }
]}
expandedRender={(row) => <div>Full profile for {row.name}</div>}
onSelectionChange={(keys, rows) => console.log(keys, rows)}
// React-only: scoped inline styles (auto-removed on unmount)
gsx={{
'.gs-table-th': { textTransform: 'uppercase', letterSpacing: '0.04em' },
'.gs-table-row:hover .gs-table-td': { background: '#faf5ff' }
}}
/>
</>
);
}The CSS is injected automatically by the component (once per document) — no
stylesheet import is required in the React build. The styles are also available
as a string at @get-set/gs-table/dist/components/styles/GSTableCSS if you want
to inline them yourself.
GSTableHandle (React ref)
| Method | Description |
|---|---|
| sort(key, dir?) | Toggle / set a column's sort. dir omitted cycles asc → desc → none. |
| setSort(sort) | Replace the full sort list ({ key, dir }[]). |
| getSort() | The active sort list. |
| filter(key, value) | Set a single column filter (empty string clears it). |
| setFilters(filters) | Replace all per-column filters at once. |
| getFilters() | The active filters ({ [key]: value }). |
| setSearch(term) | Set the global search term. |
| setPage(page) | Go to a 1-based page (clamped to range). |
| setPageSize(size) | Change page size (resets to page 1). |
| getPage() | The current 1-based page. |
| getPageCount() | Total pages for the current filtered set. |
| getSelected() | The selected row keys. |
| getSelectedRows() | The selected row objects. |
| selectAll() | Select every (selectable) row in the current filtered set. |
| clearSelection() | Clear the selection. |
| toggleRow(key) | Toggle a single row's selection by key. |
| toggleExpand(key) | Toggle a single row's expansion by key. |
| getRows() | The filtered + sorted rows (all pages). |
| refresh() | Force a re-render from current state. |
Native instance methods (window.GSTable(...) return value / GSTableConfigue.instance)
The native Ref mirrors the React handle and adds a few native-only helpers:
| Method | Description |
|---|---|
| sortBy(key, dir?) | Toggle / set a column's sort (cycles when dir omitted). |
| setSort(sort) / getSort() | Replace / read the sort list. |
| filter(key, value) / setFilters(f) / getFilters() | Column filters. |
| setSearch(term) | Global search. |
| setPage(page) / setPageSize(size) / getPage() / getPageCount() | Pagination. |
| getSelected() / getSelectedRows() | Selection keys / objects. |
| selectAll() / clearSelection() / toggleRow(key) | Selection control. |
| toggleExpand(key) | Expand / collapse a row. |
| setData(rows) | Native-only — replace the row data and re-render. |
| getRows() | Filtered + sorted rows (all pages). |
| setLoading(on) | Native-only — toggle the skeleton loading state. |
| refresh() | Re-render from current state (call after mutating ref.params). |
| destroy() | Tear down, restore the container, and unregister. |
Props / options
Every option below is accepted by all four targets unless tagged. Numbers
supplied to size options are treated as px.
Data & columns
| Option | Type | Default | Description |
|---|---|---|---|
| columns | GSTableColumn[] | [] | Column definitions (see below). |
| data | T[] | [] | Row data. |
| rowKey | string \| (row, i) => string \| number | row index | Stable key per row. |
| reference | string | auto GUID | Registry key. |
GSTableColumn
| Field | Type | Description |
|---|---|---|
| key | string | Required. Unique key; also the property read from each row. |
| label | string | Header text. Defaults to a humanized key. |
| width / minWidth / maxWidth | number \| string | Column sizing. |
| align | 'left' \| 'center' \| 'right' | Cell + header alignment. Default left. |
| sortable | boolean | Participate in sorting. Default true. |
| filterable | boolean | Show a per-column filter box. Default false. |
| filterType | 'text' \| 'select' \| 'number' \| 'none' | Filter control kind. Default text. |
| filterOptions | Array<string \| {label,value}> | Explicit select options (else inferred). |
| pinned | boolean \| 'start' \| 'end' | Pin (horizontal-sticky) the column to the start or end edge while scrolling horizontally. true pins to the start. Default unpinned. Friendly alias of sticky; pinned wins if both are set. Set it in the column definition to pin by default. |
| pinnable | boolean | Optional: let the user toggle this column's pin state at runtime. Default false; the declarative pinned works without it. |
| sticky | boolean \| 'start' \| 'end' | Legacy alias of pinned — pin the column while scrolling horizontally. |
| resizable | boolean | Allow drag-resize of this column. |
| hidden | boolean | Keep in data but do not render. |
| accessor | (row) => unknown | Derive the cell value. |
| render | (ctx) => ReactNode \| string | Custom cell renderer (React node or HTML string). |
| headerRender | (col) => ReactNode \| string | Custom header renderer. |
| sortComparator | (a, b, rowA, rowB) => number | Custom sort comparator. |
| className / headerClassName | string | Extra classes on cells / header. |
The cell render context is { value, row, rowIndex, column, rowKey }.
Sorting
| Option | Type | Default | Description |
|---|---|---|---|
| sortable | boolean | true | Enable sorting globally. |
| multiSort | boolean | false | Stack sorts across columns (priority badges). |
| sort | GSTableSort[] | — | Controlled sort state ({ key, dir }[]). |
| defaultSort | GSTableSort[] | — | Uncontrolled initial sort. |
Filtering & search
| Option | Type | Default | Description |
|---|---|---|---|
| filterable | boolean | true | Show the filter row when any column is filterable. |
| filters | Record<string,string> | — | Controlled per-column filters. |
| defaultFilters | Record<string,string> | — | (React) uncontrolled initial filters. |
| searchable | boolean | false | Show the global search box. |
| search | string | — | Controlled search term. |
| defaultSearch | string | — | (React) uncontrolled initial term. |
| searchPlaceholder | string | 'Search…' | Search input placeholder. |
| filterDebounce | number | 0 | Debounce (ms) for filter/search input. |
Pagination
| Option | Type | Default | Description |
|---|---|---|---|
| pagination | 'pagination' \| 'load-more' \| 'infinite' \| false | 'pagination' | Paging mode. false shows all rows. |
| pageSize | number | 10 | Rows per page. |
| pageSizeOptions | number[] | [10, 25, 50, 100] | Page-size selector options. |
| showPageSizeSelector | boolean | true | Show the page-size selector. |
| page | number | 1 | Controlled 1-based page. |
| defaultPage | number | 1 | (React) uncontrolled initial page. |
| loadMoreText | string | 'Load more' | Load-more button label. |
Selection
| Option | Type | Default | Description |
|---|---|---|---|
| selectable | false \| 'single' \| 'multiple' | false | Row selection mode. |
| showSelectAll | boolean | true | Header select-all checkbox (multiple mode). |
| selected | Array<string \| number> | — | Controlled selected keys. |
| defaultSelected | Array<string \| number> | — | (React) uncontrolled initial selection. |
| isRowSelectable | (row, i) => boolean | — | Mark rows as unselectable. |
| selectionActions | GSTableSelectionAction[] | — | Bulk actions shown in the selection bar while rows are selected. |
| selectionBarLabel | (count) => string | `${n} selected` | Custom count label for the selection bar. |
Selection bar (bulk actions)
When selectable is on and selectionActions is a non-empty array, selecting
one or more rows shows a selection bar above the table: the selected count,
one button per action, and a clear (×) button.
Each GSTableSelectionAction:
| Field | Type | Description |
|---|---|---|
| id | string | Optional stable id. |
| label | string | Button label. |
| icon | string (native HTML) / ReactNode (React) | Leading icon. |
| danger | boolean | Destructive (red) styling. |
| disabled | boolean \| (keys, rows) => boolean | Disable statically or per selection. |
| onClick | (keys, rows, ctx) => void | Receives the selected keys + rows; ctx.clear() clears the selection. |
<GSTable
columns={columns}
data={data}
rowKey="id"
selectable="multiple"
selectionActions={[
{ label: 'Export', icon: '⬇', onClick: (keys, rows) => exportRows(rows) },
{ label: 'Merge', disabled: (keys) => keys.length < 2, onClick: merge },
{ label: 'Delete', danger: true, onClick: (keys, _rows, ctx) => { removeAll(keys); ctx.clear(); } }
]}
/>Vanilla works the same way — pass selectionActions in the params object.
Expandable rows
| Option | Type | Default | Description |
|---|---|---|---|
| expandable | boolean | false | Enable expandable detail rows. |
| expandedRender | (row, i) => ReactNode \| string | — | Detail renderer. |
| expanded | Array<string \| number> | — | Controlled expanded keys. |
| defaultExpanded | Array<string \| number> | — | (React) uncontrolled initial expanded. |
| singleExpand | boolean | false | Only one row expanded at a time. |
Sticky · resizable · reorderable
| Option | Type | Default | Description |
|---|---|---|---|
| stickyHeader | boolean | true | Pin the header while the body scrolls. When on, the per-column filter row is pinned right below the header (its offset is measured from the real header height, so it never overlaps the first data rows). |
| maxHeight | number \| string | — | Body height before it scrolls. |
| resizableColumns | boolean | false | Enable drag-resize on all columns. |
| reorderableColumns | boolean | false | Enable drag-and-drop header reordering. |
Pinning columns. Pin individual columns with the per-column
pinnedoption (seeGSTableColumn). The table always sits in a horizontally-scrollable viewport (overflow-x: auto), so pinned columns stay fixed at thestart/endedge while the rest scroll under them. Pinned offsets are cumulative and include the selection-checkbox / expander lead columns. Pinning respects RTL (start = right) and works in both the native and React targets.
Appearance
| Option | Type | Default | Description |
|---|---|---|---|
| variant | GSTableVariant \| GSTableVariant[] | — | Visual variants (stack). |
| zebra | boolean | false | Shorthand for the striped variant. |
| density | 'comfortable' \| 'cozy' \| 'compact' | 'comfortable' | Row + control density. |
| size | 'sm' \| 'md' \| 'lg' | 'md' | Font / control scale. |
| theme | 'light' \| 'dark' \| 'auto' | 'auto' | Visual theme (auto follows the OS). |
| accentColor | string | #2563eb | Accent (CSS var --gst-accent). |
| rtl | boolean | false | Right-to-left layout. |
| caption | string | — | Table caption (visible + screen-reader). |
| customClass | GSTableCustomClass | — | Per-part class overrides. |
| className | string | — | Extra class on the root. |
State: loading / empty
| Option | Type | Default | Description |
|---|---|---|---|
| loading | boolean | false | Show skeleton loading rows. |
| skeletonRows | number | pageSize | Number of skeleton rows. |
| emptyText | string | 'No records to display' | Empty-state message. |
| emptyRender | ReactNode \| string | — | Custom empty-state content. |
React-only
| Option | Type | Description |
|---|---|---|
| gsx | NestedCSS | Scoped inline styles injected under [data-key], removed on unmount. |
Callbacks (honored in every target)
| Callback | Signature |
|---|---|
| onSort | (sort: GSTableSort[]) => void |
| onFilter | (filters, search) => void |
| onPageChange | (info: { page, pageSize, totalRows, totalPages }) => void |
| onSelectionChange | (keys, rows) => void |
| onExpandChange | (expanded: Array<string \| number>) => void |
| onRowClick | (row, index, event) => void |
| onColumnResize | (key, width) => void |
| onColumnReorder | (order: string[]) => void |
| onRowContextMenu | (row, index, event) => void |
Row context menu
GSTable ships an optional, self-contained per-row context menu. It has no
dependency on any other @get-set package — the menu (positioning, keyboard,
a11y, animation) is built in. Enable it by supplying rowContextMenu (a static
item list or a per-row builder) and/or onRowContextMenu.
Behaviour (identical in the native and React targets):
- Right-clicking a data row (not the header / filter row) fires
onRowContextMenu(row, index, event). - If
rowContextMenuyields a non-empty array, the native browser menu is suppressed (preventDefault) and the built-in menu opens at the cursor, with viewport flip (opens up / left near the edges) and shift-into-view. One level ofchildrensubmenus is supported (flips left on overflow). - The menu dismisses on outside pointer-down,
Escape, scroll, or window blur. Clicking an item runs itsonClick(row, index, { close })then closes. - Keyboard:
↑/↓move the highlight (skipping disabled + separators, with wrap),Home/Endjump to the first / last item,Enter/Spaceactivate,→/←open / close a submenu (inverted in RTL),Escapecloses. Focus moves into the menu on open and is restored to the previously focused element on close. - a11y:
role="menu"on the container,role="menuitem"on items,role="separator"on dividers,aria-disabledon disabled items. - Honors the table theme tokens (
accentColor,theme,rtl,size,density) andprefers-reduced-motionfor the entrance animation.
GSTableMenuItem schema
| Field | Type | Description |
|---|---|---|
| id | string | Optional stable id. |
| label | string | Visible label. |
| icon | HTML string (native) / ReactNode (React) | Leading icon. |
| shortcut | string | Trailing shortcut hint (non-interactive). |
| disabled | boolean | Shown but not interactive. |
| danger | boolean | Render in the destructive color. |
| separator | boolean | Render as a divider (also '-' shorthand). |
| onClick | (row, index, { close }) => void | Activation handler. |
| children | GSTableMenuItem[] | One nested submenu level. |
rowContextMenu is either an array of items (a plain '-' string is a
separator shorthand) or a builder (row, index) => items | false | null |
undefined. Returning false / null / undefined / [] suppresses the menu
for that row (but onRowContextMenu still fires).
Vanilla example
window.GSTable('#grid', {
columns,
data,
rowKey: 'id',
onRowContextMenu: (row, index) => console.log('right-clicked', row, index),
rowContextMenu: (row) => [
{ label: 'Edit', icon: '✏️', shortcut: 'E', onClick: (r) => edit(r) },
{
label: 'Move to',
children: [
{ label: 'Inbox', onClick: (r) => moveTo(r, 'inbox') },
{ label: 'Archive', onClick: (r) => moveTo(r, 'archive') }
]
},
'-',
{
label: 'Delete',
danger: true,
disabled: row.locked === true,
onClick: (r, i, { close }) => {
remove(r);
close();
}
}
]
});React example
<GSTable
columns={columns}
data={data}
rowKey="id"
onRowContextMenu={(row, index) => console.log(row, index)}
rowContextMenu={(row) => [
{ label: 'Edit', icon: <EditIcon />, shortcut: 'E', onClick: (r) => edit(r) },
{ label: 'Duplicate', onClick: (r) => duplicate(r) },
'-',
{
label: 'Delete',
danger: true,
onClick: (r, i, { close }) => {
remove(r);
close();
}
}
]}
/>Variant catalog
Variants are stackable — pass an array (variant={['striped', 'hover', 'bordered']}).
| Variant | Effect |
|---|---|
| striped | Zebra row backgrounds (same as zebra: true). |
| bordered | Vertical borders between every cell. |
| hover | Row highlight on hover. |
| compact | Tighter padding (same as density: 'compact'). |
Density & size
| density | Row padding |
|---|---|
| comfortable | Roomy (default). |
| cozy | Medium. |
| compact | Tight. |
| size | Font scale |
|---|---|
| sm | 0.8125rem |
| md | 0.9375rem (default) |
| lg | 1.0625rem |
Filter types
| filterType | Behaviour |
|---|---|
| text | Case-insensitive substring match (default). |
| select | Exact match against a dropdown (options inferred or via filterOptions). |
| number | Numeric with >, <, >=, <=, = operators (e.g. >=100). |
| none | No filter control. |
customClass parts
root, toolbar, search, table, thead, headerRow, headerCell,
filterRow, body, row, cell, footer, pagination, empty, loading.
Accessibility & keyboard
- The table uses
role="grid"; sortable headers exposearia-sortand are focusable (tabindex="0"). - Enter / Space on a focused header toggles its sort.
- Selection controls carry
aria-labels; selected rows setaria-selected. - Expand toggles set
aria-expanded. - All animations honor
prefers-reduced-motion.
Changelog
Unreleased
- Pinnable columns. New per-column
pinned(boolean | 'start' | 'end', a friendly alias ofsticky) and optionalpinnableruntime-toggle flag. Pinned columns are horizontal-sticky with correct cumulative offsets (counting the selection-checkbox / expander lead columns), an edge shadow, and RTL support — in both native and React. - Responsive horizontal scroll. The table viewport now always scrolls
horizontally (
overflow-x: auto,-webkit-overflow-scrolling: touch) with a readable min-width, so a wide table no longer breaks the page layout. This viewport is the containing block for pinned columns. - Fix: filter row no longer overlaps the top data rows. With a sticky
header, the sticky per-column filter row's
topis measured from the real header-row height (--gst-header-h) instead of a fixed guess, so the body starts cleanly beneath it. - Fix: unselectable rows are no longer painted as selected. A row for which
isRowSelectablereturnsfalsenow gets a mutedgs-table-row-unselectablestyle and can never carry the selected-row highlight.
License
ISC © Get-Set
