Skip to content

jrodrigopuca/BetterTable

Repository files navigation

better-table

npm version License: Apache 2.0 TypeScript React Tests

A modern, flexible, and fully typed data table component for React. Zero dependencies beyond React (only clsx for class merging).

✨ Features

  • 🎯 TypeScript First β€” Full generic typing for data and columns
  • πŸ“± Responsive β€” Table on desktop, cards on mobile
  • πŸ” Search & Filter β€” Global search with debounce + floating filters / filter panel / both
  • ↕️ Sorting β€” Single & multi-sort with 3-state cycle
  • πŸ‘οΈ Column Visibility β€” Show/hide columns at runtime
  • πŸ”„ Column Resizing β€” Drag-to-resize with min/max constraints
  • πŸ“‚ Expandable Rows β€” Detail rows with controlled/uncontrolled + accordion mode
  • ⚑ Virtualization β€” Built-in row virtualization for large datasets (zero deps)
  • 🌐 Server-Side Mode β€” Delegate sorting, filtering, pagination to your API
  • πŸ“Œ Sticky Headers β€” Headers stay visible on scroll
  • πŸ“„ Pagination β€” Client-side or server-side with page size changer
  • βœ… Selection β€” Single or multiple with global/row actions
  • 🎬 Row Actions β€” Callbacks, modals, links + overflow menu
  • 🌍 i18n β€” Preset locales (EN/ES/PT) + custom overrides
  • β™Ώ Accessible β€” ARIA labels, aria-live announcements, focus trap in modals
  • 🎨 Themeable β€” CSS variables, custom renderers, class overrides
  • πŸ“ Dot Notation β€” Access nested data (user.profile.name)
  • πŸ“¦ Lightweight β€” ~8KB gzipped (JS) + ~3KB (CSS)

πŸ“¦ Installation

npm install better-table

πŸš€ Quick Start

import { BetterTable } from "better-table";
import "better-table/styles.css";
import type { Column } from "better-table";

interface User {
	[key: string]: unknown;
	id: number;
	name: string;
	email: string;
}

const columns: Column<User>[] = [
	{ id: "name", accessor: "name", header: "Name", sortable: true },
	{ id: "email", accessor: "email", header: "Email" },
];

const data: User[] = [
	{ id: 1, name: "John", email: "john@example.com" },
	{ id: 2, name: "Jane", email: "jane@example.com" },
];

function App() {
	return <BetterTable<User> data={data} columns={columns} rowKey="id" />;
}

πŸ“– Examples

Sorting & Column Visibility

<BetterTable<User>
	data={users}
	columns={columns}
	rowKey="id"
	multiSort
	columnVisibility
/>

Search, Pagination & Actions

<BetterTable<User>
	data={users}
	columns={columns}
	rowKey="id"
	searchable
	searchDebounceMs={300}
	pagination={{ pageSize: 10, showSizeChanger: true }}
	rowActions={[
		{ id: "edit", label: "Edit", icon: "✏️", mode: "callback", onClick: (row) => handleEdit(row) },
		{ id: "delete", label: "Delete", mode: "callback", variant: "danger", onClick: (row) => handleDelete(row) },
	]}
	globalActions={[
		{ id: "export", label: "Export", onClick: (selected, all) => exportData(all) },
	]}
	selectionMode="multiple"
/>

Column Resizing

const columns: Column<User>[] = [
	{ id: "name", accessor: "name", header: "Name", resizable: true, minWidth: 100 },
	{ id: "email", accessor: "email", header: "Email", resizable: true, minWidth: 150, maxWidth: 400 },
];

<BetterTable<User> data={users} columns={columns} rowKey="id" resizable />

Expandable Rows

<BetterTable<User>
	data={users}
	columns={columns}
	rowKey="id"
	expandable={{
		render: (row) => (
			<div>
				<h4>Details for {row.name}</h4>
				<p>Email: {row.email}</p>
			</div>
		),
		accordion: true, // Only one row expanded at a time
	}}
/>

Server-Side Data

const [page, setPage] = useState(1);
const [sort, setSort] = useState<SortState>({ columnId: null, direction: "asc" });
const { data, total, loading } = useServerData({ page, sort });

<BetterTable<User>
	data={data}
	columns={columns}
	rowKey="id"
	loading={loading}
	manualPagination
	manualSorting
	pagination={{ pageSize: 10, totalItems: total }}
	onPageChange={(newPage) => setPage(newPage)}
	onSortChange={(newSort) => setSort(newSort)}
/>

Virtualization (Large Datasets)

// Auto-enables when pagination={false} and dataset > 500 rows
<BetterTable<User>
	data={largeDataset} // 10K+ rows
	columns={columns}
	rowKey="id"
	pagination={false}
	stickyHeader
/>

// Or explicitly control it
<BetterTable<User>
	data={largeDataset}
	columns={columns}
	rowKey="id"
	virtualize
	rowHeight={48}
	virtualBuffer={10}
/>

Filter Modes

// Floating filters inline in header (default)
<BetterTable data={data} columns={columns} />

// Collapsible filter panel
<BetterTable data={data} columns={columns} filterMode="panel" />

// Both: floating filters + panel toggle
<BetterTable data={data} columns={columns} filterMode="both" />

Internationalization

// Spanish preset
<BetterTable data={data} columns={columns} locale="es" />

// Custom overrides
<BetterTable data={data} columns={columns} locale={{ noData: "Nothing here", search: "Find..." }} />

🎨 Theming

BetterTable uses CSS Variables for customization:

:root {
	--bt-primary-color: #3b82f6;
	--bt-primary-hover: #2563eb;
	--bt-danger-color: #ef4444;
	--bt-bg-color: #ffffff;
	--bt-header-bg: #f8fafc;
	--bt-row-hover: #f1f5f9;
	--bt-row-selected: #eff6ff;
	--bt-text-color: #1e293b;
	--bt-border-color: #e2e8f0;
	--bt-border-radius: 8px;
	--bt-cell-padding: 12px 16px;
}

Dark Mode

[data-theme="dark"] {
	--bt-bg-color: #1e1e1e;
	--bt-header-bg: #2d2d2d;
	--bt-row-hover: #333333;
	--bt-text-color: #e0e0e0;
	--bt-border-color: #404040;
}

πŸ“š API Reference

BetterTable Props

Prop Type Default Description
data T[] required Array of data to display
columns Column<T>[] required Column definitions
rowKey keyof T | (row: T, index: number) => string 'id' Unique identifier for each row
Search & Filter
searchable boolean false Enable global search
searchColumns string[] all columns Columns to search in
searchDebounceMs number 300 Search debounce delay
filterMode 'floating' | 'panel' | 'both' 'floating' Filter display mode
Sorting
multiSort boolean false Enable multi-column sorting
sort SortState - Controlled sort state
onSortChange (sort: SortState) => void - Sort change handler
Pagination
pagination PaginationConfig | false { pageSize: 10 } Pagination settings
onPageChange (page, size) => void - Page change handler
Selection
selectionMode 'single' | 'multiple' - Selection mode
onSelectionChange (selected: T[]) => void - Selection change handler
Actions
rowActions RowAction<T>[] - Per-row actions
globalActions GlobalAction<T>[] - Global toolbar actions
maxVisibleActions number 3 Actions before overflow menu
Column Features
resizable boolean false Enable column resizing globally
columnVisibility boolean false Show column visibility toggle
Expandable Rows
expandable ExpandableConfig<T> - Expandable row configuration
Virtualization
virtualize boolean auto Enable row virtualization
rowHeight number 48 Fixed row height (px)
virtualBuffer number 5 Buffer rows above/below viewport
Server-Side Mode
manualSorting boolean false Skip client-side sorting
manualFiltering boolean false Skip client-side filtering
manualPagination boolean false Skip client-side pagination
Appearance
stickyHeader boolean false Sticky table header
striped boolean false Striped rows
bordered boolean false Bordered cells
size 'small' | 'medium' | 'large' 'medium' Table density
locale 'en' | 'es' | 'pt' | TableLocale 'en' Locale preset or custom strings
loading boolean false Show loading state
className string - Additional CSS class
classNames TableClassNames - Per-element class overrides

Column Definition

Prop Type Description
id string Unique column identifier
accessor string Data accessor (supports dot notation)
header string | ReactNode Column header content
type 'string' | 'number' | 'boolean' | 'date' Data type for filtering/sorting
sortable boolean Enable sorting
filterable boolean Enable column filter
resizable boolean Enable column resizing
minWidth number Minimum width when resizing (px)
maxWidth number Maximum width when resizing (px)
cell (value, row, index) => ReactNode Custom cell renderer
width string | number Column width
align 'left' | 'center' | 'right' Text alignment

πŸ› οΈ Development

git clone https://github.com/jrodrigopuca/BetterTable.git
cd BetterTable

# Install all dependencies (requires pnpm)
pnpm install

# Run demo app
pnpm dev

# Run tests (watch mode)
pnpm test

# Run tests (single run)
pnpm test:run

# Type check
pnpm lint

# Build library
pnpm build

Storybook

pnpm storybook       # Dev server on port 6006
pnpm build-storybook  # Production build

πŸ“ Project Structure

BetterTable/
β”œβ”€β”€ better-table/              # Library source
β”‚   β”œβ”€β”€ src/
β”‚   β”‚   β”œβ”€β”€ components/
β”‚   β”‚   β”‚   └── BetterTable/
β”‚   β”‚   β”‚       β”œβ”€β”€ components/    # Table sub-components
β”‚   β”‚   β”‚       β”œβ”€β”€ context/       # 6 focused React contexts
β”‚   β”‚   β”‚       β”œβ”€β”€ hooks/         # Custom hooks
β”‚   β”‚   β”‚       β”œβ”€β”€ styles/        # CSS styles
β”‚   β”‚   β”‚       β”œβ”€β”€ utils/         # Helper functions
β”‚   β”‚   β”‚       └── types.ts       # TypeScript definitions
β”‚   β”‚   └── index.ts               # Library entry point ("use client")
β”‚   β”œβ”€β”€ demo/                      # Demo application
β”‚   └── dist/                      # Built library
β”œβ”€β”€ storybook/                     # Storybook (separate project)
β”œβ”€β”€ .github/workflows/             # CI/CD (tests + Storybook deploy)
└── README.md

πŸ“„ License

Apache License 2.0 Β© Juan Rodrigo Puca

🀝 Contributing

Contributions, issues and feature requests are welcome!

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

πŸ”— Links

About

A modern, flexible, and fully typed data table component for React.

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors