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

@facetui/react

v1.0.1

Published

Production-ready, accessible data table for React and Next.js — built on React Aria Components + TanStack Table + Tailwind CSS.

Readme

FacetUI

npm  types  React 18  license

FacetUI: The enterprise-grade, fully accessible React grid that replaces the sortable, filterable, paginated table you'd otherwise rebuild on every project — headless architecture, conditional row selection, and a real controlled filtering pipeline, all styled natively in Tailwind with zero lock-in. Drop it in for a working table in one line, or drive the raw hook yourself when you need total control — same engine, three levels of abstraction. Built for teams who need WCAG-grade accessibility and server-side data out of the box, not bolted on after a support ticket.

A production-ready, fully accessible data table component for React and Next.js. Built with TypeScript, React Aria Components for the interactive primitives, TanStack Table for the state engine, and Tailwind CSS v4 for styling. Follows the headless architecture pattern so every visual detail is customizable without forking the logic.


Why FacetUI?

Most table libraries force a trade-off: feature-rich but rigid (hard to restyle, locked into someone else's CSS framework) or headless but primitive (you re-implement sorting, pagination, and selection logic from scratch on every project).

FacetUI is built to eliminate that trade-off — it ships as a complete, enterprise-grade data grid engine with the styling boundary drawn exactly where you want it.

  • Headless architecture, batteries included. Drop in <DataTable> for a fully working grid in one line, reach for the individual primitives to compose a custom layout, or drive the raw useDataTable() engine directly when you need total control over rendering. One state engine, three levels of abstraction — pick the altitude that fits the screen you're building.
  • Conditional row selection, not just row selection. Selection isn't a blunt on/off switch. Pass a predicate — enableRowSelection={(row) => row.original.status === "active"} — and the checkbox column, select-all logic, and keyboard interactions all automatically respect it. Locked, archived, or permission-gated rows simply can't be selected, in bulk or individually.
  • A real controlled filtering pipeline. Global search and per-column filters compose together, work identically whether the data source is a local array or a paginated API, and expose both controlled and uncontrolled state so you can wire them into a URL, a form, or React Query without fighting the component.
  • Zero style lock-in. Every element exposes a classNames override key with Tailwind-conflict-safe merging. Replace entire regions — toolbar, empty state, loading overlay — through render slots instead of forking component internals.
  • Accessibility engineered in, not patched on. WCAG 2.1 AA-targeted out of the box: correct grid semantics, aria-sort, live-region announcements, and React Aria-backed keyboard interaction on every interactive control. Ship accessible tables without becoming an accessibility expert.

Features at a Glance

| Capability | What you get | |---|---| | Sorting | Client-side or server-driven (manualSorting), single or multi-column (Shift-click), full keyboard support | | Pagination | Client-side slicing or server-driven (manualPagination + rowCount), configurable page-size presets | | Global search | Case-insensitive substring match across all columns, controlled or uncontrolled, debounced-friendly API | | Column filters | Per-column filter state (columnFilters) that composes with global search — bring your own filter UI | | Row selection | Single or multi-select, bulk select-all, and a per-row predicate for conditional/locked rows | | Column visibility | Built-in toggle menu; hide, show, or lock columns per your ColumnDef | | Styling | Tailwind v4-native with classNames overrides at every element — no CSS-in-JS, no shadow DOM. Themed via ~16 semantic OKLCH tokens (THEMING.md) | | Render slots | Replace toolbar, empty state, loading overlay, or row wrapper without touching internals | | Accessibility | role="grid" semantics, aria-sort, aria-live filter/pagination announcements, full keyboard navigation | | Framework fit | Next.js App Router ready — every entry point ships "use client" out of the box | | TypeScript | Fully generic over your row shape — column defs, cell renderers, and the table instance are all type-safe |


Installation

1. Install from npm

npm install @facetui/react

Then the peer dependencies (React and ReactDOM you already have):

npm install react-aria-components lucide-react clsx tailwind-merge

@tanstack/react-table is a direct dependency and installs automatically.

Commercial license. @facetui/react is licensed per developer — see LICENSE.md for terms.

2. Tailwind CSS v4

The utility classes are auto-detected — there is no content array to update. You only need the FacetUI token contract in a global stylesheet: copy the :root / .dark blocks and the @theme inline mapping from src/index.css. If your project already uses a shadcn/ui v4 theme, the ~16 tokens (--background, --primary, --muted, --ring, …) are identical and already in place.

See THEMING.md for the full token reference, dark mode, and building your own theme or presets.

On Tailwind v3? Add "./node_modules/@facetui/react/dist/**/*.js" to your content globs, register the tokens under theme.extend.colors, and mind the v3→v4 utility renames (shadow-xs, rounded-xs, outline-hidden). Details in THEMING.md.

3. Next.js App Router

Every entry point ships "use client" — no extra configuration.


Alternative: vendor the source

Licensees who prefer to copy the component into their own tree rather than depend on the package:

  1. Copy src/components/data-table/ into your project.
  2. npm install react-aria-components @tanstack/react-table lucide-react clsx tailwind-merge
  3. On Tailwind v3, add "./src/components/data-table/**/*.{ts,tsx}" to your content globs.
  4. Import from your local path (e.g. @/components/data-table) instead of @facetui/react — everything below is otherwise identical.

Usage

Basic example

import { DataTable } from "@facetui/react";
import type { ColumnDef } from "@facetui/react";

interface User {
  id: string;
  name: string;
  email: string;
  role: "admin" | "editor" | "viewer";
}

const columns: ColumnDef<User>[] = [
  { id: "name",  header: "Name",  accessorKey: "name"  },
  { id: "email", header: "Email", accessorKey: "email" },
  { id: "role",  header: "Role",  accessorKey: "role"  },
];

const data: User[] = [
  { id: "1", name: "Alice Johnson", email: "[email protected]", role: "admin"  },
  { id: "2", name: "Bob Smith",     email: "[email protected]",   role: "editor" },
  { id: "3", name: "Carol White",   email: "[email protected]", role: "viewer" },
];

export default function UsersPage() {
  return (
    <DataTable
      data={data}
      columns={columns}
      aria-label="Users table"
      getRowId={(row) => row.id}
    />
  );
}

This gives you: global search, client-side sorting on every column, column visibility toggle, row-level pagination, and a loading state — all with keyboard navigation and screen-reader support.


Advanced example — custom cells, row selection, server-side data, and style overrides

"use client";

import { useState } from "react";
import { DataTable } from "@facetui/react";
import type {
  ColumnDef,
  PaginationState,
  SortingState,
} from "@facetui/react";

interface Product {
  id: string;
  name: string;
  category: string;
  price: number;
  stock: number;
  status: "in_stock" | "low_stock" | "out_of_stock";
}

// ── Column definitions ──────────────────────────────────────────────────────

const columns: ColumnDef<Product>[] = [
  {
    id: "name",
    header: "Product",
    accessorKey: "name",
    size: 220,
  },
  {
    id: "category",
    header: "Category",
    accessorKey: "category",
    size: 140,
  },
  {
    id: "price",
    header: "Price",
    accessorKey: "price",
    size: 100,
    // Custom cell — format as currency
    cell: ({ value }) => (
      <span className="font-mono tabular-nums">
        {new Intl.NumberFormat("en-US", {
          style: "currency",
          currency: "USD",
        }).format(value as number)}
      </span>
    ),
  },
  {
    id: "stock",
    header: "Stock",
    accessorKey: "stock",
    size: 80,
    cell: ({ value }) => (
      <span className="tabular-nums">{(value as number).toLocaleString()}</span>
    ),
  },
  {
    id: "status",
    header: "Status",
    accessorKey: "status",
    size: 120,
    enableSorting: false,
    // Custom cell — status badge
    cell: ({ value }) => {
      const label = String(value).replace(/_/g, " ");
      const styles = {
        in_stock:      "bg-green-100 text-green-800",
        low_stock:     "bg-yellow-100 text-yellow-800",
        out_of_stock:  "bg-red-100   text-red-800",
      } as const;
      return (
        <span
          className={`inline-flex items-center rounded-full px-2 py-0.5
                      text-xs font-medium capitalize
                      ${styles[value as Product["status"]]}`}
        >
          {label}
        </span>
      );
    },
  },
];

// ── Page component ──────────────────────────────────────────────────────────

export default function ProductsPage() {
  // Lift sort + pagination state so changes trigger your API
  const [sorting, setSorting]       = useState<SortingState[]>([]);
  const [pagination, setPagination] = useState<PaginationState>({
    pageIndex: 0,
    pageSize: 10,
  });

  // Replace with your real data-fetching hook (React Query, SWR, etc.)
  const { data, rowCount, isLoading } = useProducts({ sorting, pagination });

  return (
    <DataTable
      // ── Data ───────────────────────────────────────────────────────────
      data={data}
      columns={columns}
      getRowId={(row) => row.id}
      isLoading={isLoading}

      // ── Server-side mode ───────────────────────────────────────────────
      manualSorting
      manualPagination
      manualFiltering
      rowCount={rowCount}
      onSortingChange={setSorting}
      onPaginationChange={setPagination}

      // ── Row selection ─────────────────────────────────────────────────
      enableRowSelection
      enableMultiRowSelection
      onRowSelectionChange={(selected) => {
        console.log("Selected IDs:", Object.keys(selected));
      }}

      // ── Layout & density ──────────────────────────────────────────────
      density="compact"
      pageSizeOptions={[10, 25, 50]}
      aria-label="Products inventory table"

      // ── Tailwind class overrides ──────────────────────────────────────
      classNames={{
        root:    "rounded-xl shadow-sm",
        th:      "text-xs uppercase tracking-wider",
        tr:      "odd:bg-muted/30",
        toolbar: "px-1",
      }}

      // ── Render slots ──────────────────────────────────────────────────
      renderToolbar={(table) => (
        <div className="flex items-center justify-between py-2">
          <h2 className="text-lg font-semibold">
            Products
            <span className="ml-2 text-sm font-normal text-muted-foreground">
              ({table.filteredRowCount.toLocaleString()} items)
            </span>
          </h2>
          <div className="flex gap-2">
            <input
              type="search"
              placeholder="Search products…"
              onChange={(e) => table.setGlobalFilter(e.target.value)}
              className="h-8 rounded-md border border-input bg-background px-3 text-sm"
            />
            <button
              type="button"
              onClick={() => alert(`Exporting ${Object.keys(table.rowSelection).length} rows`)}
              className="rounded-md bg-primary px-3 py-1.5 text-sm text-primary-foreground"
            >
              Export selected
            </button>
          </div>
        </div>
      )}

      renderEmpty={() => (
        <div className="flex flex-col items-center gap-3 py-20">
          <p className="text-base font-medium">No products found</p>
          <p className="text-sm text-muted-foreground">
            Try a different search term or clear your filters.
          </p>
        </div>
      )}

      // Make each row navigable — works with Next.js <Link> too
      renderRowWrapper={(row, children) => (
        <tr
          key={row.id}
          role="row"
          tabIndex={0}
          onClick={() => window.location.assign(`/products/${row.original.id}`)}
          onKeyDown={(e) => {
            if (e.key === "Enter" || e.key === " ")
              window.location.assign(`/products/${row.original.id}`);
          }}
          className="cursor-pointer"
          aria-label={`View details for ${row.original.name}`}
        >
          {children}
        </tr>
      )}
    />
  );
}

Using the headless hook directly

When you need a completely custom render layer, bypass all primitives and drive the table yourself:

"use client";

import { useDataTable } from "@facetui/react";

export function MyCustomTable() {
  const table = useDataTable({ data, columns, enableRowSelection: true });

  return (
    <div>
      <input
        value={table.globalFilter}
        onChange={(e) => table.setGlobalFilter(e.target.value)}
      />
      {table.rows.map((row) => (
        <div key={row.id} onClick={() => row.toggleSelected()}>
          {table.columns.map((col) => (
            <span key={col.id}>{/* your own render logic */}</span>
          ))}
        </div>
      ))}
    </div>
  );
}

Props API

<DataTable> props

All props from ColumnDef, PaginationOptions, SortingOptions, FilteringOptions, RowSelectionOptions, and RenderSlots are accepted at the top level.

Core

| Prop | Type | Default | Description | |---|---|---|---| | data | TData[] | required | The array of data records to display. | | columns | ColumnDef<TData>[] | required | Column definitions. See ColumnDef below. | | getRowId | (row: TData, index: number) => string | Row array index | Derive a stable unique id for each row. Provide this whenever rows can be reordered or filtered to prevent selection state from drifting. | | isLoading | boolean | false | Displays an animated overlay above the table. Does not unmount existing rows. | | density | "compact" \| "default" \| "comfortable" | "default" | Controls cell padding across the entire table. | | classNames | ClassNameOverrides | {} | Tailwind class overrides per element. See ClassNameOverrides. | | style | CSSProperties | — | Inline styles applied to the root <div>. | | aria-label | string | — | Accessible label for the <table> element. Required for WCAG compliance when there is no visible caption. | | aria-describedby | string | — | ID of an element that describes the table's purpose. |

Pagination options

| Prop | Type | Default | Description | |---|---|---|---| | manualPagination | boolean | false | When true, the component renders data as-is and does not slice it. You must handle slicing in your data-fetching layer. | | rowCount | number | — | Total rows across all pages. Required when manualPagination is true so the page count can be calculated. | | pageSizeOptions | number[] | [10, 25, 50, 100] | The page-size choices rendered in the pagination bar's dropdown. | | onPaginationChange | (state: PaginationState) => void | — | Called whenever the page index or page size changes. Use this to re-fetch from your API. |

Sorting options

| Prop | Type | Default | Description | |---|---|---|---| | manualSorting | boolean | false | When true, disables client-side sorting. The component fires onSortingChange and waits for you to pass sorted data back via data. | | enableMultiSort | boolean | false | Allow multiple columns to be sorted simultaneously. Users hold Shift to add a secondary sort. | | onSortingChange | (state: SortingState[]) => void | — | Called whenever the sort state changes. Each entry is { id: string; desc: boolean }. |

Filtering options

| Prop | Type | Default | Description | |---|---|---|---| | manualFiltering | boolean | false | When true, disables client-side filtering. The component fires onGlobalFilterChange and renders whatever is in data. | | globalFilter | string | — | Controlled global filter value. Provide this to sync the search input with external state (e.g., a URL search param). | | onGlobalFilterChange | (value: string) => void | — | Called on every keystroke in the search input. | | columnFilters | ColumnFiltersState (Array<{ id: string; value: unknown }>) | [] | Per-column filter values. Applied as a case-insensitive substring match, ANDed together and combined with globalFilter. Controlled if provided, uncontrolled otherwise — pair with table.setColumnFilters for imperative control. | | onColumnFiltersChange | (filters: ColumnFiltersState) => void | — | Called whenever column-level filters change. |

Row selection options

| Prop | Type | Default | Description | |---|---|---|---| | enableRowSelection | boolean \| ((row: Row<TData>) => boolean) | undefined | true (or a function) auto-injects the checkbox column — no manual column setup required. Pass a function to make selectability conditional per row (e.g., (row) => !row.original.locked); rows that fail the check can't be selected individually or via select-all. Pass false to disable row selection entirely. | | enableMultiRowSelection | boolean | true | When false, selecting a new row automatically deselects the previous one. | | onRowSelectionChange | (state: RowSelectionState) => void | — | Called whenever selection changes. State is a Record<rowId, true>. |

Render slots

| Prop | Type | Description | |---|---|---| | renderToolbar | (table: TableInstance<TData>) => ReactNode | Replaces the entire default toolbar (search input + column toggle). Receives the live table instance so you can call table.setGlobalFilter, table.rowSelection, etc. | | renderEmpty | () => ReactNode | Replaces the default empty state shown when rows.length === 0 and isLoading is false. | | renderLoading | () => ReactNode | Replaces the default spinner overlay shown when isLoading is true. | | renderRowWrapper | (row: Row<TData>, children: ReactNode) => ReactNode | Wraps each <tr>. Use this to make rows into Next.js <Link> elements, add onClick handlers, or attach drag-and-drop attributes. The children are the rendered <td> elements — you must render them inside your wrapper. |


ColumnDef<TData>

Passed as an element of the columns array.

| Property | Type | Default | Description | |---|---|---|---| | id | string | required | Unique column identifier. Used as the sort key, visibility key, and React key. | | header | ReactNode \| ((ctx: HeaderContext<TData>) => ReactNode) | required | Column header content. Pass a plain string for simple labels or a render function for custom headers with sort indicators, tooltips, etc. | | accessorKey | keyof TData | — | Key on the data object to read the cell value from. Provide either this or accessorFn, not both. | | accessorFn | (row: TData) => unknown | — | Function to derive the cell value. Use for computed values or deeply nested fields (e.g., row.address.city). | | cell | (ctx: CellContext<TData>) => ReactNode | Raw string | Custom cell renderer. Receives { row, column, value, table }. The pre-extracted value saves you from calling the accessor yourself. | | enableSorting | boolean | true | Show a sort toggle on this column's header. Set to false for columns where sorting is meaningless (e.g., action columns). | | enableHiding | boolean | true | Include this column in the column-visibility toggle. Set to false for columns that must always be visible (e.g., a primary name column). | | enableResizing | boolean | false | Reserved for a future column-resize implementation. | | size | number | 150 | Initial column width in pixels, applied as a CSS width on the <th> and <td>. | | minSize | number | — | Minimum column width. Enforced when column resizing is enabled. | | maxSize | number | — | Maximum column width. Enforced when column resizing is enabled. | | meta | Record<string, unknown> | — | Arbitrary metadata. Useful for passing flags into custom cell or header renderers without adding them to the data shape. |


ClassNameOverrides

Passed to the classNames prop. Every key is optional. Values are Tailwind class strings; conflicts are resolved by tailwind-merge.

| Key | Targets | |---|---| | root | The outermost <div> wrapping the entire component. | | table | The <table> element. | | thead | The <thead> element. | | theadRow | The <tr> inside <thead>. | | th | Every <th> cell. | | tbody | The <tbody> element. | | tr | Every data <tr> in <tbody>. | | td | Every <td> cell. | | toolbar | The toolbar <div> (search + column toggle). | | pagination | The pagination <div> (rows-per-page + navigation). |


TableInstance<TData>

The live table object passed to all render slots and accessible via useDataTableContext() in any child component.

| Member | Type | Description | |---|---|---| | rows | Row<TData>[] | The current page's rows after filtering and sorting. | | columns | RuntimeColumn<TData>[] | Column definitions augmented with live state methods. | | pagination | PaginationState | Current { pageIndex, pageSize }. | | sorting | SortingState[] | Active sort entries: [{ id, desc }]. | | rowSelection | RowSelectionState | A Record<rowId, true> of selected rows. | | globalFilter | string | Current search input value. | | columnFilters | ColumnFiltersState | Active per-column filter values. | | pageCount | number | Total number of pages. | | filteredRowCount | number | Total rows after filtering — use for "N results" status text. | | getIsAllRowsSelected() | () => boolean | Returns true if every row on the current page is selected. | | getIsSomeRowsSelected() | () => boolean | Returns true if at least one (but not all) rows are selected. | | toggleAllRowsSelected(value?) | (value?: boolean) => void | Select or deselect all rows on the current page. | | setSorting(updater) | (updater) => void | Imperatively set sorting state. Accepts a value or an updater function. | | setPagination(updater) | (updater) => void | Imperatively set pagination state. Accepts a value or an updater function. | | setGlobalFilter(value) | (value: string) => void | Imperatively set the global search filter and reset to page 0. | | setColumnFilters(updater) | (updater) => void | Imperatively set per-column filters and reset to page 0. Accepts a value or an updater function. |


Headless hooks (standalone)

Each hook can be used independently of <DataTable> when building fully custom table layouts.

| Hook | Purpose | |---|---| | useDataTable(props) | The complete table state engine. Returns a TableInstance. | | useColumnSort(options?) | Sort state with a three-state toggle cycle (none → asc → desc → none). | | usePagination(options?) | Pagination state with goToPage, nextPage, previousPage, and setPageSize helpers. | | useRowSelection(options?) | Row selection state with toggleRow, toggleAllRows, and clearSelection. | | useColumnVisibility(columns) | Column show/hide state with toggleColumn, showAll, and hideAll. | | useGlobalFilter(columns, options?) | Global filter state with a filterData(data) helper for client-side use. | | useDataTableContext<TData>() | Access the nearest <DataTable>'s TableInstance from any descendant component. |


Accessibility

FacetUI targets WCAG 2.1 Level AA compliance.

| Requirement | Implementation | |---|---| | Grid semantics | <table role="grid"> with role="row", role="columnheader", and role="gridcell" on all descendants. | | Column sort state | aria-sort="ascending \| descending \| none" on every sortable <th>. | | Row position | aria-rowindex on every <tr> so assistive technology can report position in the full dataset, not just the current page. | | Row count | aria-rowcount on <table> reflects the true total. | | Loading state | aria-busy="true" on <table> while isLoading is active; overlay has role="status". | | Live regions | Filter result count and page info use aria-live="polite" aria-atomic="true". | | Keyboard navigation | Sort triggers and nav buttons are plain <button>s; the page-size selector and column-visibility menu are React Aria Components Select / Menu — fully reachable and activatable via keyboard, with focus containment while open. | | Checkboxes | React Aria Components Checkbox provides role="checkbox", correct checked / indeterminate state (aria-checked="mixed" for the select-all tri-state), and a focus ring. | | Focus indicators | All interactive elements have focus-visible:ring-2 focus-visible:ring-ring — compliant with WCAG 2.4.11. | | Icon-only buttons | All icon-only elements carry aria-label or aria-hidden="true" to prevent decoration from being announced. |


License

Commercial license — see LICENSE.md for full terms. In short: use the Software freely inside your own projects; redistributing, reselling, or publishing the source code itself is not permitted without written consent.

Security

Found a vulnerability? Please don't open a public issue — see SECURITY.md for how to report it responsibly.