@kedman1234/react-light-table
v2.2.0
Published
A lightweight, accessible, sortable, searchable, and selectable React table component for any dataset
Maintainers
Readme
@kedman1234/react-light-table
A lightweight, accessible, sortable, searchable, and selectable React table component for any dataset. Built with TypeScript, zero dependencies beyond React.
Features
- Real-time search - filter rows instantly across all visible columns
- Column sorting - click to sort ascending/descending on any column
- Row selection - individual and select-all with callback support
- Pagination - built-in page navigation with customizable page size
- Column visibility - toggle columns on/off via dropdown menu
- Column pinning - pin any column to the left or right edge via sticky positioning
- Row virtualization - render thousands of rows efficiently (only visible rows in DOM)
- Controlled props - take full ownership of search, sort, page, and selection state
- Custom cell render - return any JSX per cell with the
renderprop - Theming - full CSS custom properties for easy customization
- Accessible - ARIA roles, labels, and keyboard navigation
- Lightweight - small bundle, zero runtime dependencies
- TypeScript - full type safety with generics support
- Dual data source - works with local arrays or remote URLs
Installation
npm install @kedman1234/react-light-tableNote: react (>=18.0.0) and react-dom (>=18.0.0) are required as peer dependencies.
Quick Start
With Local Data
import { Table } from "@kedman1234/react-light-table";
import "@kedman1234/react-light-table/dist/table.css";
interface User {
id: number;
name: string;
email: string;
age: number;
}
const columns = [
{ key: "name", path: "name", label: "Name", sortable: true },
{ key: "email", path: "email", label: "Email", sortable: true },
{ key: "age", path: "age", label: "Age", sortable: true },
];
const data: User[] = [
{ id: 1, name: "Alice", email: "[email protected]", age: 30 },
{ id: 2, name: "Bob", email: "[email protected]", age: 25 },
];
function App() {
return (
<Table<User>
columns={columns}
data={data}
rowKey="id"
isSearchable
isSelectable
pageSize={10}
bordered
striped
/>
);
}With Remote URL
<Table<User>
columns={columns}
url="https://jsonplaceholder.typicode.com/users"
rowKey="id"
isSearchable
emptyMessage="No users found"
errorMessage="Failed to load users"
/>The built-in fetch is deliberately constrained. If any of these do not hold it renders the error state instead of data:
- scheme must be
http:orhttps:(SSRF guard) - 30-second timeout (
AbortController) - response
Content-Typemust includeapplication/json - response body must be under 10 MB
- parsed JSON must be a top-level array
For authenticated APIs, proxies, or non-array payloads, fetch in your app and pass data instead.
API Reference
<Table> Props
| Prop | Type | Default | Description |
|---|---|---|---|
| columns | ColumnDef<T>[] | required | Column definitions array |
| data | T[] | - | Local data array (takes priority over url) |
| url | string | - | Remote data URL (triggers fetch) |
| rowKey | keyof T & string | 'id' | Field to use as unique row identifier |
| className | string | '' | Additional CSS class(es) for the table |
| isSearchable | boolean | false | Show search input |
| isSelectable | boolean | false | Show selection checkboxes |
| searchableFields | string[] | all visible columns | Which column paths to search |
| pageSize | number | - | Rows per page (omit to show all) |
| loading | boolean | false | External loading state control |
| emptyMessage | string \| ReactNode | 'No data available' | Empty state message |
| errorMessage | string \| ReactNode | 'Failed to load data' | Error state message |
| stickyHeader | boolean | false | Sticky table header |
| striped | boolean | false | Striped row styling |
| bordered | boolean | false | Bordered table styling |
| virtualized | boolean | false | Enable row virtualization (only visible rows rendered) |
| expandable | { render: (row) => ReactNode; expandedByDefault?: boolean } | - | Render expandable sub-rows spanning the full table width |
| exportCsv | boolean | false | Show an Export CSV button for the current filtered+sorted dataset |
| onSelectionChange | (rows: T[]) => void | - | Callback when selection changes |
| onSort | (column, direction) => void | - | Callback when sort changes |
| onPageChange | (page: number) => void | - | Callback when page changes |
Controlled props - when provided, the component defers state management to the parent:
| Prop | Type | Paired callback | Description |
|---|---|---|---|
| searchValue | string | onSearchChange | Controlled search text |
| onSearchChange | (text: string) => void | - | Called on every search input change |
| sortState | SortState | onSortChange | Controlled sort state |
| onSortChange | (state: SortState) => void | - | Called when user clicks a sort button |
| page | number | onPageChange | Controlled current page (1-based) |
| selectedRows | T[] | onSelectionChange | Controlled selected rows |
Column Definition (ColumnDef<T>)
| Property | Type | Required | Description |
|---|---|---|---|
| key | string | Yes | Unique column identifier |
| path | keyof T & string | Yes | Data object property to display |
| label | string | Yes | Column header text |
| sortable | boolean | No | Enable sorting for this column |
| isVisible | boolean | No | Initial visibility (default: true) |
| className | string | No | CSS class for column cells |
| pin | 'left' \| 'right' | No | Pin column to the left or right edge |
| formatter | (value, row) => ReactNode | No | Simple cell transform (string/number output) |
| render | (value, row) => ReactNode | No | Full JSX cell renderer - takes precedence over formatter |
Sortable columns:
pathis used as a flatrow[path]lookup and must match/^\w+$/(word characters only, no dots, dashes or spaces, and not a prototype-access name). Apaththat fails this check logs a warning and the column simply does not sort — it never throws. For a derived or nested sort value, map your data to a flat key before passing it to the table.
SortState type
interface SortState {
key: string;
direction: 'asc' | 'desc' | 'none';
}Examples
Custom Cell Renderer (render prop)
Use render when you need full JSX control over a cell - badges, links, buttons, icons:
const columns = [
{
key: "status",
path: "status",
label: "Status",
render: (value, row) => (
<span style={{ color: value === "active" ? "green" : "red", fontWeight: 600 }}>
{String(value).toUpperCase()}
</span>
),
},
];Use formatter for simple string transforms (value -> string):
{
key: "salary",
path: "salary",
label: "Salary",
formatter: (value) => `$${Number(value).toLocaleString()}`,
}Column Pinning
Pin columns to the left or right so they remain visible during horizontal scroll:
const columns = [
{ key: "id", path: "id", label: "ID", pin: "left" },
{ key: "name", path: "name", label: "Name", pin: "left" },
// ... scrollable middle columns ...
{ key: "status", path: "status", label: "Status", pin: "right" },
];
<Table
columns={columns}
data={data}
rowKey="id"
bordered
/>Use the CSS custom properties to adjust pinned-column appearance:
:root {
--rlt-pin-cell-bg: #fff; /* pinned body cell background */
--rlt-pin-header-bg: #f8f9fa; /* pinned header cell background */
--rlt-pin-shadow-left: 2px 0 6px rgba(0, 0, 0, 0.15);
--rlt-pin-shadow-right: -2px 0 6px rgba(0, 0, 0, 0.15);
}Row Virtualization
Render large datasets efficiently - only visible rows (+ 10 buffer rows) are in the DOM:
<Table
columns={columns}
data={largeDataset} // e.g. 10 000 rows
rowKey="id"
virtualized
stickyHeader
/>Control the visible container height via CSS:
:root {
--rlt-virtual-height: 600px; /* default: 400px */
}Controlled Props
Take full ownership of search, sort, page, and selection - useful for URL-synced tables or server-side data:
import { useState } from "react";
import { Table } from "@kedman1234/react-light-table";
import type { SortState } from "@kedman1234/react-light-table";
function MyTable() {
const [search, setSearch] = useState("");
const [sort, setSort] = useState<SortState>({ key: "", direction: "none" });
const [page, setPage] = useState(1);
const [selected, setSelected] = useState<User[]>([]);
return (
<Table<User>
columns={columns}
data={data}
rowKey="id"
isSelectable
pageSize={20}
searchValue={search}
onSearchChange={setSearch}
sortState={sort}
onSortChange={setSort}
page={page}
onPageChange={setPage}
selectedRows={selected}
onSelectionChange={setSelected}
/>
);
}Each controlled dimension is independent - you can control just searchValue and leave the rest uncontrolled.
Row Expansion
<Table<User>
columns={columns}
data={data}
rowKey="id"
expandable={{
render: (row) => <div>{row.email}</div>,
expandedByDefault: false,
}}
/>CSV Export
<Table<User>
columns={columns}
data={data}
isSearchable
exportCsv
/>The CSV export uses the current filtered and sorted dataset, not the raw input array. Cell values with leading =, +, -, or @ are sanitized before download to reduce spreadsheet formula-injection risk.
Column Resizing And Keyboard Navigation
Column resize handles are available on every visible column header. The grid follows the ARIA APG grid pattern: Tab enters the grid at a single cell (roving tabindex), then arrow keys move focus between neighboring cells. The roving cell resets to the first header whenever the grid changes shape (sort, search, page, column visibility). The column visibility menu traps Tab focus while open and closes with Escape.
Selection Callback
<Table
columns={columns}
data={data}
isSelectable
onSelectionChange={(selected) => {
console.log("Selected rows:", selected);
}}
/>Pagination
<Table
columns={columns}
data={data}
pageSize={25}
onPageChange={(page) => {
console.log("Current page:", page);
}}
/>Theming with CSS Variables
Customize the entire look using CSS custom properties:
:root {
--rlt-font-family: "Inter", sans-serif;
--rlt-header-bg: #1a1a2e;
--rlt-header-color: #eee;
--rlt-row-hover-bg: #f0f4ff;
--rlt-row-selected-bg: #d4edff;
--rlt-border-color: #e0e0e0;
--rlt-sort-icon-color: #999;
--rlt-sort-active-color: #1a73e8;
--rlt-search-border-color: #ccc;
--rlt-search-focus-border: #1a73e8;
--rlt-pagination-active-bg: #1a73e8;
--rlt-pagination-active-color: #fff;
--rlt-cell-padding: 10px 14px;
}Available CSS Variables
| Variable | Default | Description |
|---|---|---|
| --rlt-font-family | inherit | Font family |
| --rlt-font-size | 14px | Base font size |
| --rlt-header-bg | #f8f9fa | Header background |
| --rlt-header-color | #333 | Header text color |
| --rlt-header-font-weight | 600 | Header font weight |
| --rlt-row-hover-bg | #f5f5f5 | Row hover background |
| --rlt-row-selected-bg | #e3f2fd | Selected row background |
| --rlt-row-striped-bg | #fafafa | Striped row background |
| --rlt-border-color | #dee2e6 | Border color |
| --rlt-sort-icon-color | #666 | Sort icon color |
| --rlt-sort-active-color | #333 | Active sort icon color |
| --rlt-search-border-color | #ccc | Search input border |
| --rlt-search-focus-border | #4a90d9 | Search input focus border |
| --rlt-search-bg | #fff | Search input background |
| --rlt-control-bg | #fff | Background for control chrome (column menu, pagination / export / expand buttons) — override for dark themes |
| --rlt-checkbox-size | 24px | Row-select checkbox size (WCAG 2.5.8 minimum is 24px) |
| --rlt-cell-padding | 12px 15px | Cell padding |
| --rlt-pagination-padding | 12px 16px | Pagination bar padding (horizontal inset aligns it with cell padding) |
| --rlt-pagination-active-bg | #4a90d9 | Active page button bg |
| --rlt-pagination-active-color | #fff | Active page button color |
| --rlt-virtual-height | 400px | Virtualized scroll container height |
| --rlt-pin-cell-bg | inherit | Pinned body cell background |
| --rlt-pin-header-bg | #f8f9fa | Pinned header cell background |
| --rlt-pin-shadow-left | 2px 0 5px rgba(0,0,0,0.12) | Pinned left column shadow |
| --rlt-pin-shadow-right | -2px 0 5px rgba(0,0,0,0.12) | Pinned right column shadow |
Dark mode: override
--rlt-control-bg(and the other background tokens) under your dark selector — the control chrome reads that token rather than a hardcoded#fff.Hover affordances: if you render your own row-action controls inside cells, give their hover state a tint distinct from
--rlt-row-hover-bg(and don't derive both from the same base color) so an action's hover stays visible over an already-hovered row.
Exported Hooks
The library also exports the internal hooks for advanced use cases:
useSort<T>(data, onSort?, sortState?, onSortChange?)- Sort state management; also returnssortSkipped(true when a dataset larger than 100,000 rows is left unsorted)useSearch<T>(data, searchableFields?, searchValue?, onSearchChange?)- Search/filter logic; also returnssearchTruncated(true when a query over 200 chars is ignored).<Table>renders a visible notice for both cases.useSelection<T>(data, rowKey, onSelectionChange?, selectedRows?)- Selection managementusePagination<T>(data, pageSize?, onPageChange?, page?)- Pagination logic
Each hook supports the same controlled/uncontrolled pattern as the <Table> component.
Browser Support
- Chrome (last 2 versions)
- Firefox (last 2 versions)
- Safari (last 2 versions)
- Edge (last 2 versions)
Contributing
- Fork the repository
- Create your feature branch:
git checkout -b feature/my-feature - Install dependencies:
npm install - Run the demo:
npm run dev - Run tests:
npm test - Commit your changes:
git commit -m 'Add my feature' - Push to the branch:
git push origin feature/my-feature - Open a Pull Request
License
MIT (c) Kedar Vijay Kulkarni
