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

@kedman1234/react-light-table

v2.2.0

Published

A lightweight, accessible, sortable, searchable, and selectable React table component for any dataset

Readme

@kedman1234/react-light-table

npm version License: MIT TypeScript

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 render prop
  • 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-table

Note: 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: or https: (SSRF guard)
  • 30-second timeout (AbortController)
  • response Content-Type must include application/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: path is used as a flat row[path] lookup and must match /^\w+$/ (word characters only, no dots, dashes or spaces, and not a prototype-access name). A path that 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 returns sortSkipped (true when a dataset larger than 100,000 rows is left unsorted)
  • useSearch<T>(data, searchableFields?, searchValue?, onSearchChange?) - Search/filter logic; also returns searchTruncated (true when a query over 200 chars is ignored). <Table> renders a visible notice for both cases.
  • useSelection<T>(data, rowKey, onSelectionChange?, selectedRows?) - Selection management
  • usePagination<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

  1. Fork the repository
  2. Create your feature branch: git checkout -b feature/my-feature
  3. Install dependencies: npm install
  4. Run the demo: npm run dev
  5. Run tests: npm test
  6. Commit your changes: git commit -m 'Add my feature'
  7. Push to the branch: git push origin feature/my-feature
  8. Open a Pull Request

License

MIT (c) Kedar Vijay Kulkarni