@facetui/react 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +60 -0
- package/README.md +509 -0
- package/THEMING.md +168 -0
- package/dist/index.cjs +1220 -0
- package/dist/index.d.cts +367 -0
- package/dist/index.d.ts +367 -0
- package/dist/index.js +1209 -0
- package/package.json +122 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# License
|
|
2
|
+
|
|
3
|
+
**FacetUI — Commercial License**
|
|
4
|
+
|
|
5
|
+
Copyright © 2026 Cameron Gates. All rights reserved.
|
|
6
|
+
|
|
7
|
+
This is a commercial software license. By purchasing, downloading, cloning, or otherwise obtaining a copy of this software and its associated source code (the "Software"), you ("Licensee") agree to be bound by the terms below. If you do not agree to these terms, do not use the Software.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 1. Grant of License
|
|
12
|
+
|
|
13
|
+
Subject to payment of any applicable license fee and compliance with this Agreement, the copyright holder ("Licensor") grants Licensee a non-exclusive, non-transferable, worldwide license to:
|
|
14
|
+
|
|
15
|
+
- Use, modify, and integrate the Software into an unlimited number of personal or commercial end-products ("Projects") that Licensee builds;
|
|
16
|
+
- Distribute the Software **only as compiled/bundled output** embedded within a Project (e.g., as part of a built web application), not as standalone or redistributable source code.
|
|
17
|
+
|
|
18
|
+
Each individual developer requires their own license. A team or organizational license, if purchased, covers the number of seats specified at time of purchase.
|
|
19
|
+
|
|
20
|
+
## 2. Restrictions
|
|
21
|
+
|
|
22
|
+
Licensee may **not**, without prior written consent from Licensor:
|
|
23
|
+
|
|
24
|
+
- Resell, sublicense, rent, lease, or otherwise redistribute the Software's source code, in whole or in part, whether modified or unmodified;
|
|
25
|
+
- Publish the Software (or a derivative of it) to any public package registry (e.g., npm, GitHub Packages) in a way that makes the source code freely obtainable by third parties;
|
|
26
|
+
- Use the Software to create a competing product — i.e., a data table component or library offered for sale, license, or free public distribution;
|
|
27
|
+
- Remove, obscure, or alter any copyright, trademark, or attribution notices contained in the Software;
|
|
28
|
+
- Claim authorship of the Software or represent it as your own original work.
|
|
29
|
+
|
|
30
|
+
## 3. Ownership
|
|
31
|
+
|
|
32
|
+
The Software is licensed, not sold. Licensor retains all right, title, and interest in and to the Software, including all intellectual property rights therein. This Agreement does not grant Licensee any rights to Licensor's trademarks, service marks, or trade names.
|
|
33
|
+
|
|
34
|
+
## 4. Support & Updates
|
|
35
|
+
|
|
36
|
+
Unless otherwise stated at the point of purchase, this license does not entitle Licensee to ongoing support or future version updates. Where support or updates are offered (e.g., for a limited period, or under a separate maintenance agreement), their scope is as described at the point of purchase.
|
|
37
|
+
|
|
38
|
+
## 5. Warranty Disclaimer
|
|
39
|
+
|
|
40
|
+
THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. LICENSOR DOES NOT WARRANT THAT THE SOFTWARE WILL BE ERROR-FREE OR UNINTERRUPTED.
|
|
41
|
+
|
|
42
|
+
## 6. Limitation of Liability
|
|
43
|
+
|
|
44
|
+
IN NO EVENT SHALL LICENSOR BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. LICENSOR'S TOTAL LIABILITY UNDER THIS AGREEMENT SHALL NOT EXCEED THE AMOUNT PAID BY LICENSEE FOR THE SOFTWARE.
|
|
45
|
+
|
|
46
|
+
## 7. Termination
|
|
47
|
+
|
|
48
|
+
This license terminates automatically if Licensee breaches any term of this Agreement. Upon termination, Licensee must cease all use of the Software and destroy all copies of the source code in their possession. Projects already distributed to end users prior to termination are not affected.
|
|
49
|
+
|
|
50
|
+
## 8. Governing Law
|
|
51
|
+
|
|
52
|
+
This Agreement is governed by the laws of **New South Wales, Australia**, without regard to conflict-of-law principles. The parties submit to the exclusive jurisdiction of the courts of New South Wales and the courts competent to hear appeals from them.
|
|
53
|
+
|
|
54
|
+
## 9. Contact
|
|
55
|
+
|
|
56
|
+
Questions about licensing terms, team/enterprise licensing, or requests for permissions beyond this license should be directed to **contact@redbeard.au**. See also `SECURITY.md` for vulnerability reports.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
> **Note:** This document is a commercial license template, not a substitute for legal advice. Replace the bracketed placeholders above and have it reviewed by a lawyer licensed in your jurisdiction before relying on it for a paid marketplace listing.
|
package/README.md
ADDED
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
# FacetUI
|
|
2
|
+
|
|
3
|
+
**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.
|
|
4
|
+
|
|
5
|
+
A production-ready, fully accessible data table component for React and Next.js. Built with TypeScript, [React Aria Components](https://react-spectrum.adobe.com/react-aria/) for the interactive primitives, [TanStack Table](https://tanstack.com/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.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Why FacetUI?
|
|
10
|
+
|
|
11
|
+
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).
|
|
12
|
+
|
|
13
|
+
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.
|
|
14
|
+
|
|
15
|
+
- **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.
|
|
16
|
+
- **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.
|
|
17
|
+
- **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.
|
|
18
|
+
- **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.
|
|
19
|
+
- **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.
|
|
20
|
+
|
|
21
|
+
### Features at a Glance
|
|
22
|
+
|
|
23
|
+
| Capability | What you get |
|
|
24
|
+
|---|---|
|
|
25
|
+
| **Sorting** | Client-side or server-driven (`manualSorting`), single or multi-column (`Shift`-click), full keyboard support |
|
|
26
|
+
| **Pagination** | Client-side slicing or server-driven (`manualPagination` + `rowCount`), configurable page-size presets |
|
|
27
|
+
| **Global search** | Case-insensitive substring match across all columns, controlled or uncontrolled, debounced-friendly API |
|
|
28
|
+
| **Column filters** | Per-column filter state (`columnFilters`) that composes with global search — bring your own filter UI |
|
|
29
|
+
| **Row selection** | Single or multi-select, bulk select-all, and a per-row predicate for conditional/locked rows |
|
|
30
|
+
| **Column visibility** | Built-in toggle menu; hide, show, or lock columns per your `ColumnDef` |
|
|
31
|
+
| **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`](./THEMING.md)) |
|
|
32
|
+
| **Render slots** | Replace toolbar, empty state, loading overlay, or row wrapper without touching internals |
|
|
33
|
+
| **Accessibility** | `role="grid"` semantics, `aria-sort`, `aria-live` filter/pagination announcements, full keyboard navigation |
|
|
34
|
+
| **Framework fit** | Next.js App Router ready — every entry point ships `"use client"` out of the box |
|
|
35
|
+
| **TypeScript** | Fully generic over your row shape — column defs, cell renderers, and the table instance are all type-safe |
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
Get FacetUI running in your project in under five minutes.
|
|
42
|
+
|
|
43
|
+
**1. Copy the component folder into your project:**
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
src/components/data-table/
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**2. Install peer dependencies:**
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npm install react-aria-components @tanstack/react-table lucide-react clsx tailwind-merge
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
> Installing the published `@facetui/react` package pulls in `@tanstack/react-table` for you; the line above is the full list for the copy-the-folder route.
|
|
56
|
+
|
|
57
|
+
**3. Tailwind CSS v4 — no config step.** The utility classes are auto-detected from your source; there is no `content` array to update. (Still on Tailwind v3? Add `"./src/components/data-table/**/*.{ts,tsx}"` to your `content` globs and treat the v3→v4 utility renames in the components — `shadow-xs`, `rounded-xs`, `outline-hidden` — accordingly.)
|
|
58
|
+
|
|
59
|
+
**4. Provide the FacetUI token contract.** The components are written against ~16 semantic CSS custom properties (`--background`, `--foreground`, `--border`, `--primary`, `--primary-foreground`, `--muted`, `--muted-foreground`, `--accent`, `--popover`, `--destructive`, `--ring`, …). Copy the `:root` / `.dark` blocks and the `@theme inline` mapping from [`src/index.css`](./src/index.css) into your global stylesheet — or, if your project already uses a [shadcn/ui](https://ui.shadcn.com) **v4** theme, the tokens are identical and already in place. See [`THEMING.md`](./THEMING.md) for the full token reference, dark mode, and a walkthrough for building your own theme or presets.
|
|
60
|
+
|
|
61
|
+
> **Next.js App Router note:** All files include `"use client"` — no extra configuration required.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Usage
|
|
66
|
+
|
|
67
|
+
### Basic example
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
import { DataTable } from "@/components/data-table";
|
|
71
|
+
import type { ColumnDef } from "@/components/data-table";
|
|
72
|
+
|
|
73
|
+
interface User {
|
|
74
|
+
id: string;
|
|
75
|
+
name: string;
|
|
76
|
+
email: string;
|
|
77
|
+
role: "admin" | "editor" | "viewer";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const columns: ColumnDef<User>[] = [
|
|
81
|
+
{ id: "name", header: "Name", accessorKey: "name" },
|
|
82
|
+
{ id: "email", header: "Email", accessorKey: "email" },
|
|
83
|
+
{ id: "role", header: "Role", accessorKey: "role" },
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
const data: User[] = [
|
|
87
|
+
{ id: "1", name: "Alice Johnson", email: "alice@example.com", role: "admin" },
|
|
88
|
+
{ id: "2", name: "Bob Smith", email: "bob@example.com", role: "editor" },
|
|
89
|
+
{ id: "3", name: "Carol White", email: "carol@example.com", role: "viewer" },
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
export default function UsersPage() {
|
|
93
|
+
return (
|
|
94
|
+
<DataTable
|
|
95
|
+
data={data}
|
|
96
|
+
columns={columns}
|
|
97
|
+
aria-label="Users table"
|
|
98
|
+
getRowId={(row) => row.id}
|
|
99
|
+
/>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
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.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
### Advanced example — custom cells, row selection, server-side data, and style overrides
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
"use client";
|
|
112
|
+
|
|
113
|
+
import { useState } from "react";
|
|
114
|
+
import { DataTable } from "@/components/data-table";
|
|
115
|
+
import type {
|
|
116
|
+
ColumnDef,
|
|
117
|
+
PaginationState,
|
|
118
|
+
SortingState,
|
|
119
|
+
} from "@/components/data-table";
|
|
120
|
+
|
|
121
|
+
interface Product {
|
|
122
|
+
id: string;
|
|
123
|
+
name: string;
|
|
124
|
+
category: string;
|
|
125
|
+
price: number;
|
|
126
|
+
stock: number;
|
|
127
|
+
status: "in_stock" | "low_stock" | "out_of_stock";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── Column definitions ──────────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
const columns: ColumnDef<Product>[] = [
|
|
133
|
+
{
|
|
134
|
+
id: "name",
|
|
135
|
+
header: "Product",
|
|
136
|
+
accessorKey: "name",
|
|
137
|
+
size: 220,
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
id: "category",
|
|
141
|
+
header: "Category",
|
|
142
|
+
accessorKey: "category",
|
|
143
|
+
size: 140,
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: "price",
|
|
147
|
+
header: "Price",
|
|
148
|
+
accessorKey: "price",
|
|
149
|
+
size: 100,
|
|
150
|
+
// Custom cell — format as currency
|
|
151
|
+
cell: ({ value }) => (
|
|
152
|
+
<span className="font-mono tabular-nums">
|
|
153
|
+
{new Intl.NumberFormat("en-US", {
|
|
154
|
+
style: "currency",
|
|
155
|
+
currency: "USD",
|
|
156
|
+
}).format(value as number)}
|
|
157
|
+
</span>
|
|
158
|
+
),
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
id: "stock",
|
|
162
|
+
header: "Stock",
|
|
163
|
+
accessorKey: "stock",
|
|
164
|
+
size: 80,
|
|
165
|
+
cell: ({ value }) => (
|
|
166
|
+
<span className="tabular-nums">{(value as number).toLocaleString()}</span>
|
|
167
|
+
),
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
id: "status",
|
|
171
|
+
header: "Status",
|
|
172
|
+
accessorKey: "status",
|
|
173
|
+
size: 120,
|
|
174
|
+
enableSorting: false,
|
|
175
|
+
// Custom cell — status badge
|
|
176
|
+
cell: ({ value }) => {
|
|
177
|
+
const label = String(value).replace(/_/g, " ");
|
|
178
|
+
const styles = {
|
|
179
|
+
in_stock: "bg-green-100 text-green-800",
|
|
180
|
+
low_stock: "bg-yellow-100 text-yellow-800",
|
|
181
|
+
out_of_stock: "bg-red-100 text-red-800",
|
|
182
|
+
} as const;
|
|
183
|
+
return (
|
|
184
|
+
<span
|
|
185
|
+
className={`inline-flex items-center rounded-full px-2 py-0.5
|
|
186
|
+
text-xs font-medium capitalize
|
|
187
|
+
${styles[value as Product["status"]]}`}
|
|
188
|
+
>
|
|
189
|
+
{label}
|
|
190
|
+
</span>
|
|
191
|
+
);
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
];
|
|
195
|
+
|
|
196
|
+
// ── Page component ──────────────────────────────────────────────────────────
|
|
197
|
+
|
|
198
|
+
export default function ProductsPage() {
|
|
199
|
+
// Lift sort + pagination state so changes trigger your API
|
|
200
|
+
const [sorting, setSorting] = useState<SortingState[]>([]);
|
|
201
|
+
const [pagination, setPagination] = useState<PaginationState>({
|
|
202
|
+
pageIndex: 0,
|
|
203
|
+
pageSize: 10,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Replace with your real data-fetching hook (React Query, SWR, etc.)
|
|
207
|
+
const { data, rowCount, isLoading } = useProducts({ sorting, pagination });
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<DataTable
|
|
211
|
+
// ── Data ───────────────────────────────────────────────────────────
|
|
212
|
+
data={data}
|
|
213
|
+
columns={columns}
|
|
214
|
+
getRowId={(row) => row.id}
|
|
215
|
+
isLoading={isLoading}
|
|
216
|
+
|
|
217
|
+
// ── Server-side mode ───────────────────────────────────────────────
|
|
218
|
+
manualSorting
|
|
219
|
+
manualPagination
|
|
220
|
+
manualFiltering
|
|
221
|
+
rowCount={rowCount}
|
|
222
|
+
onSortingChange={setSorting}
|
|
223
|
+
onPaginationChange={setPagination}
|
|
224
|
+
|
|
225
|
+
// ── Row selection ─────────────────────────────────────────────────
|
|
226
|
+
enableRowSelection
|
|
227
|
+
enableMultiRowSelection
|
|
228
|
+
onRowSelectionChange={(selected) => {
|
|
229
|
+
console.log("Selected IDs:", Object.keys(selected));
|
|
230
|
+
}}
|
|
231
|
+
|
|
232
|
+
// ── Layout & density ──────────────────────────────────────────────
|
|
233
|
+
density="compact"
|
|
234
|
+
pageSizeOptions={[10, 25, 50]}
|
|
235
|
+
aria-label="Products inventory table"
|
|
236
|
+
|
|
237
|
+
// ── Tailwind class overrides ──────────────────────────────────────
|
|
238
|
+
classNames={{
|
|
239
|
+
root: "rounded-xl shadow-sm",
|
|
240
|
+
th: "text-xs uppercase tracking-wider",
|
|
241
|
+
tr: "odd:bg-muted/30",
|
|
242
|
+
toolbar: "px-1",
|
|
243
|
+
}}
|
|
244
|
+
|
|
245
|
+
// ── Render slots ──────────────────────────────────────────────────
|
|
246
|
+
renderToolbar={(table) => (
|
|
247
|
+
<div className="flex items-center justify-between py-2">
|
|
248
|
+
<h2 className="text-lg font-semibold">
|
|
249
|
+
Products
|
|
250
|
+
<span className="ml-2 text-sm font-normal text-muted-foreground">
|
|
251
|
+
({table.filteredRowCount.toLocaleString()} items)
|
|
252
|
+
</span>
|
|
253
|
+
</h2>
|
|
254
|
+
<div className="flex gap-2">
|
|
255
|
+
<input
|
|
256
|
+
type="search"
|
|
257
|
+
placeholder="Search products…"
|
|
258
|
+
onChange={(e) => table.setGlobalFilter(e.target.value)}
|
|
259
|
+
className="h-8 rounded-md border border-input bg-background px-3 text-sm"
|
|
260
|
+
/>
|
|
261
|
+
<button
|
|
262
|
+
type="button"
|
|
263
|
+
onClick={() => alert(`Exporting ${Object.keys(table.rowSelection).length} rows`)}
|
|
264
|
+
className="rounded-md bg-primary px-3 py-1.5 text-sm text-primary-foreground"
|
|
265
|
+
>
|
|
266
|
+
Export selected
|
|
267
|
+
</button>
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
)}
|
|
271
|
+
|
|
272
|
+
renderEmpty={() => (
|
|
273
|
+
<div className="flex flex-col items-center gap-3 py-20">
|
|
274
|
+
<p className="text-base font-medium">No products found</p>
|
|
275
|
+
<p className="text-sm text-muted-foreground">
|
|
276
|
+
Try a different search term or clear your filters.
|
|
277
|
+
</p>
|
|
278
|
+
</div>
|
|
279
|
+
)}
|
|
280
|
+
|
|
281
|
+
// Make each row navigable — works with Next.js <Link> too
|
|
282
|
+
renderRowWrapper={(row, children) => (
|
|
283
|
+
<tr
|
|
284
|
+
key={row.id}
|
|
285
|
+
role="row"
|
|
286
|
+
tabIndex={0}
|
|
287
|
+
onClick={() => window.location.assign(`/products/${row.original.id}`)}
|
|
288
|
+
onKeyDown={(e) => {
|
|
289
|
+
if (e.key === "Enter" || e.key === " ")
|
|
290
|
+
window.location.assign(`/products/${row.original.id}`);
|
|
291
|
+
}}
|
|
292
|
+
className="cursor-pointer"
|
|
293
|
+
aria-label={`View details for ${row.original.name}`}
|
|
294
|
+
>
|
|
295
|
+
{children}
|
|
296
|
+
</tr>
|
|
297
|
+
)}
|
|
298
|
+
/>
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
---
|
|
304
|
+
|
|
305
|
+
### Using the headless hook directly
|
|
306
|
+
|
|
307
|
+
When you need a completely custom render layer, bypass all primitives and drive the table yourself:
|
|
308
|
+
|
|
309
|
+
```tsx
|
|
310
|
+
"use client";
|
|
311
|
+
|
|
312
|
+
import { useDataTable } from "@/components/data-table";
|
|
313
|
+
|
|
314
|
+
export function MyCustomTable() {
|
|
315
|
+
const table = useDataTable({ data, columns, enableRowSelection: true });
|
|
316
|
+
|
|
317
|
+
return (
|
|
318
|
+
<div>
|
|
319
|
+
<input
|
|
320
|
+
value={table.globalFilter}
|
|
321
|
+
onChange={(e) => table.setGlobalFilter(e.target.value)}
|
|
322
|
+
/>
|
|
323
|
+
{table.rows.map((row) => (
|
|
324
|
+
<div key={row.id} onClick={() => row.toggleSelected()}>
|
|
325
|
+
{table.columns.map((col) => (
|
|
326
|
+
<span key={col.id}>{/* your own render logic */}</span>
|
|
327
|
+
))}
|
|
328
|
+
</div>
|
|
329
|
+
))}
|
|
330
|
+
</div>
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
337
|
+
## Props API
|
|
338
|
+
|
|
339
|
+
### `<DataTable>` props
|
|
340
|
+
|
|
341
|
+
All props from `ColumnDef`, `PaginationOptions`, `SortingOptions`, `FilteringOptions`, `RowSelectionOptions`, and `RenderSlots` are accepted at the top level.
|
|
342
|
+
|
|
343
|
+
#### Core
|
|
344
|
+
|
|
345
|
+
| Prop | Type | Default | Description |
|
|
346
|
+
|---|---|---|---|
|
|
347
|
+
| `data` | `TData[]` | **required** | The array of data records to display. |
|
|
348
|
+
| `columns` | `ColumnDef<TData>[]` | **required** | Column definitions. See [ColumnDef](#columndef) below. |
|
|
349
|
+
| `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. |
|
|
350
|
+
| `isLoading` | `boolean` | `false` | Displays an animated overlay above the table. Does not unmount existing rows. |
|
|
351
|
+
| `density` | `"compact" \| "default" \| "comfortable"` | `"default"` | Controls cell padding across the entire table. |
|
|
352
|
+
| `classNames` | `ClassNameOverrides` | `{}` | Tailwind class overrides per element. See [ClassNameOverrides](#classnameoverrides). |
|
|
353
|
+
| `style` | `CSSProperties` | — | Inline styles applied to the root `<div>`. |
|
|
354
|
+
| `aria-label` | `string` | — | Accessible label for the `<table>` element. Required for WCAG compliance when there is no visible caption. |
|
|
355
|
+
| `aria-describedby` | `string` | — | ID of an element that describes the table's purpose. |
|
|
356
|
+
|
|
357
|
+
#### Pagination options
|
|
358
|
+
|
|
359
|
+
| Prop | Type | Default | Description |
|
|
360
|
+
|---|---|---|---|
|
|
361
|
+
| `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. |
|
|
362
|
+
| `rowCount` | `number` | — | Total rows across all pages. **Required** when `manualPagination` is `true` so the page count can be calculated. |
|
|
363
|
+
| `pageSizeOptions` | `number[]` | `[10, 25, 50, 100]` | The page-size choices rendered in the pagination bar's dropdown. |
|
|
364
|
+
| `onPaginationChange` | `(state: PaginationState) => void` | — | Called whenever the page index or page size changes. Use this to re-fetch from your API. |
|
|
365
|
+
|
|
366
|
+
#### Sorting options
|
|
367
|
+
|
|
368
|
+
| Prop | Type | Default | Description |
|
|
369
|
+
|---|---|---|---|
|
|
370
|
+
| `manualSorting` | `boolean` | `false` | When `true`, disables client-side sorting. The component fires `onSortingChange` and waits for you to pass sorted data back via `data`. |
|
|
371
|
+
| `enableMultiSort` | `boolean` | `false` | Allow multiple columns to be sorted simultaneously. Users hold `Shift` to add a secondary sort. |
|
|
372
|
+
| `onSortingChange` | `(state: SortingState[]) => void` | — | Called whenever the sort state changes. Each entry is `{ id: string; desc: boolean }`. |
|
|
373
|
+
|
|
374
|
+
#### Filtering options
|
|
375
|
+
|
|
376
|
+
| Prop | Type | Default | Description |
|
|
377
|
+
|---|---|---|---|
|
|
378
|
+
| `manualFiltering` | `boolean` | `false` | When `true`, disables client-side filtering. The component fires `onGlobalFilterChange` and renders whatever is in `data`. |
|
|
379
|
+
| `globalFilter` | `string` | — | Controlled global filter value. Provide this to sync the search input with external state (e.g., a URL search param). |
|
|
380
|
+
| `onGlobalFilterChange` | `(value: string) => void` | — | Called on every keystroke in the search input. |
|
|
381
|
+
| `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. |
|
|
382
|
+
| `onColumnFiltersChange` | `(filters: ColumnFiltersState) => void` | — | Called whenever column-level filters change. |
|
|
383
|
+
|
|
384
|
+
#### Row selection options
|
|
385
|
+
|
|
386
|
+
| Prop | Type | Default | Description |
|
|
387
|
+
|---|---|---|---|
|
|
388
|
+
| `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. |
|
|
389
|
+
| `enableMultiRowSelection` | `boolean` | `true` | When `false`, selecting a new row automatically deselects the previous one. |
|
|
390
|
+
| `onRowSelectionChange` | `(state: RowSelectionState) => void` | — | Called whenever selection changes. State is a `Record<rowId, true>`. |
|
|
391
|
+
|
|
392
|
+
#### Render slots
|
|
393
|
+
|
|
394
|
+
| Prop | Type | Description |
|
|
395
|
+
|---|---|---|
|
|
396
|
+
| `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. |
|
|
397
|
+
| `renderEmpty` | `() => ReactNode` | Replaces the default empty state shown when `rows.length === 0` and `isLoading` is false. |
|
|
398
|
+
| `renderLoading` | `() => ReactNode` | Replaces the default spinner overlay shown when `isLoading` is true. |
|
|
399
|
+
| `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. |
|
|
400
|
+
|
|
401
|
+
---
|
|
402
|
+
|
|
403
|
+
### `ColumnDef<TData>`
|
|
404
|
+
|
|
405
|
+
Passed as an element of the `columns` array.
|
|
406
|
+
|
|
407
|
+
| Property | Type | Default | Description |
|
|
408
|
+
|---|---|---|---|
|
|
409
|
+
| `id` | `string` | **required** | Unique column identifier. Used as the sort key, visibility key, and React key. |
|
|
410
|
+
| `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. |
|
|
411
|
+
| `accessorKey` | `keyof TData` | — | Key on the data object to read the cell value from. Provide either this or `accessorFn`, not both. |
|
|
412
|
+
| `accessorFn` | `(row: TData) => unknown` | — | Function to derive the cell value. Use for computed values or deeply nested fields (e.g., `row.address.city`). |
|
|
413
|
+
| `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. |
|
|
414
|
+
| `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). |
|
|
415
|
+
| `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). |
|
|
416
|
+
| `enableResizing` | `boolean` | `false` | Reserved for a future column-resize implementation. |
|
|
417
|
+
| `size` | `number` | `150` | Initial column width in pixels, applied as a CSS `width` on the `<th>` and `<td>`. |
|
|
418
|
+
| `minSize` | `number` | — | Minimum column width. Enforced when column resizing is enabled. |
|
|
419
|
+
| `maxSize` | `number` | — | Maximum column width. Enforced when column resizing is enabled. |
|
|
420
|
+
| `meta` | `Record<string, unknown>` | — | Arbitrary metadata. Useful for passing flags into custom cell or header renderers without adding them to the data shape. |
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
### `ClassNameOverrides`
|
|
425
|
+
|
|
426
|
+
Passed to the `classNames` prop. Every key is optional. Values are Tailwind class strings; conflicts are resolved by `tailwind-merge`.
|
|
427
|
+
|
|
428
|
+
| Key | Targets |
|
|
429
|
+
|---|---|
|
|
430
|
+
| `root` | The outermost `<div>` wrapping the entire component. |
|
|
431
|
+
| `table` | The `<table>` element. |
|
|
432
|
+
| `thead` | The `<thead>` element. |
|
|
433
|
+
| `theadRow` | The `<tr>` inside `<thead>`. |
|
|
434
|
+
| `th` | Every `<th>` cell. |
|
|
435
|
+
| `tbody` | The `<tbody>` element. |
|
|
436
|
+
| `tr` | Every data `<tr>` in `<tbody>`. |
|
|
437
|
+
| `td` | Every `<td>` cell. |
|
|
438
|
+
| `toolbar` | The toolbar `<div>` (search + column toggle). |
|
|
439
|
+
| `pagination` | The pagination `<div>` (rows-per-page + navigation). |
|
|
440
|
+
|
|
441
|
+
---
|
|
442
|
+
|
|
443
|
+
### `TableInstance<TData>`
|
|
444
|
+
|
|
445
|
+
The live table object passed to all render slots and accessible via `useDataTableContext()` in any child component.
|
|
446
|
+
|
|
447
|
+
| Member | Type | Description |
|
|
448
|
+
|---|---|---|
|
|
449
|
+
| `rows` | `Row<TData>[]` | The current page's rows after filtering and sorting. |
|
|
450
|
+
| `columns` | `RuntimeColumn<TData>[]` | Column definitions augmented with live state methods. |
|
|
451
|
+
| `pagination` | `PaginationState` | Current `{ pageIndex, pageSize }`. |
|
|
452
|
+
| `sorting` | `SortingState[]` | Active sort entries: `[{ id, desc }]`. |
|
|
453
|
+
| `rowSelection` | `RowSelectionState` | A `Record<rowId, true>` of selected rows. |
|
|
454
|
+
| `globalFilter` | `string` | Current search input value. |
|
|
455
|
+
| `columnFilters` | `ColumnFiltersState` | Active per-column filter values. |
|
|
456
|
+
| `pageCount` | `number` | Total number of pages. |
|
|
457
|
+
| `filteredRowCount` | `number` | Total rows after filtering — use for "N results" status text. |
|
|
458
|
+
| `getIsAllRowsSelected()` | `() => boolean` | Returns `true` if every row on the current page is selected. |
|
|
459
|
+
| `getIsSomeRowsSelected()` | `() => boolean` | Returns `true` if at least one (but not all) rows are selected. |
|
|
460
|
+
| `toggleAllRowsSelected(value?)` | `(value?: boolean) => void` | Select or deselect all rows on the current page. |
|
|
461
|
+
| `setSorting(updater)` | `(updater) => void` | Imperatively set sorting state. Accepts a value or an updater function. |
|
|
462
|
+
| `setPagination(updater)` | `(updater) => void` | Imperatively set pagination state. Accepts a value or an updater function. |
|
|
463
|
+
| `setGlobalFilter(value)` | `(value: string) => void` | Imperatively set the global search filter and reset to page 0. |
|
|
464
|
+
| `setColumnFilters(updater)` | `(updater) => void` | Imperatively set per-column filters and reset to page 0. Accepts a value or an updater function. |
|
|
465
|
+
|
|
466
|
+
---
|
|
467
|
+
|
|
468
|
+
### Headless hooks (standalone)
|
|
469
|
+
|
|
470
|
+
Each hook can be used independently of `<DataTable>` when building fully custom table layouts.
|
|
471
|
+
|
|
472
|
+
| Hook | Purpose |
|
|
473
|
+
|---|---|
|
|
474
|
+
| `useDataTable(props)` | The complete table state engine. Returns a `TableInstance`. |
|
|
475
|
+
| `useColumnSort(options?)` | Sort state with a three-state toggle cycle (none → asc → desc → none). |
|
|
476
|
+
| `usePagination(options?)` | Pagination state with `goToPage`, `nextPage`, `previousPage`, and `setPageSize` helpers. |
|
|
477
|
+
| `useRowSelection(options?)` | Row selection state with `toggleRow`, `toggleAllRows`, and `clearSelection`. |
|
|
478
|
+
| `useColumnVisibility(columns)` | Column show/hide state with `toggleColumn`, `showAll`, and `hideAll`. |
|
|
479
|
+
| `useGlobalFilter(columns, options?)` | Global filter state with a `filterData(data)` helper for client-side use. |
|
|
480
|
+
| `useDataTableContext<TData>()` | Access the nearest `<DataTable>`'s `TableInstance` from any descendant component. |
|
|
481
|
+
|
|
482
|
+
---
|
|
483
|
+
|
|
484
|
+
## Accessibility
|
|
485
|
+
|
|
486
|
+
FacetUI targets **WCAG 2.1 Level AA** compliance.
|
|
487
|
+
|
|
488
|
+
| Requirement | Implementation |
|
|
489
|
+
|---|---|
|
|
490
|
+
| Grid semantics | `<table role="grid">` with `role="row"`, `role="columnheader"`, and `role="gridcell"` on all descendants. |
|
|
491
|
+
| Column sort state | `aria-sort="ascending \| descending \| none"` on every sortable `<th>`. |
|
|
492
|
+
| Row position | `aria-rowindex` on every `<tr>` so assistive technology can report position in the full dataset, not just the current page. |
|
|
493
|
+
| Row count | `aria-rowcount` on `<table>` reflects the true total. |
|
|
494
|
+
| Loading state | `aria-busy="true"` on `<table>` while `isLoading` is active; overlay has `role="status"`. |
|
|
495
|
+
| Live regions | Filter result count and page info use `aria-live="polite" aria-atomic="true"`. |
|
|
496
|
+
| 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. |
|
|
497
|
+
| 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. |
|
|
498
|
+
| Focus indicators | All interactive elements have `focus-visible:ring-2 focus-visible:ring-ring` — compliant with WCAG 2.4.11. |
|
|
499
|
+
| Icon-only buttons | All icon-only elements carry `aria-label` or `aria-hidden="true"` to prevent decoration from being announced. |
|
|
500
|
+
|
|
501
|
+
---
|
|
502
|
+
|
|
503
|
+
## License
|
|
504
|
+
|
|
505
|
+
Commercial license — see [`LICENSE.md`](./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.
|
|
506
|
+
|
|
507
|
+
## Security
|
|
508
|
+
|
|
509
|
+
Found a vulnerability? Please don't open a public issue — see [`SECURITY.md`](./SECURITY.md) for how to report it responsibly.
|