Better Tables

00open source · mit licensed

Tables that reach your database

A data table is really two problems: the grid your users see, and the queries behind it. Most libraries hand you the grid and leave the SQL to you. Better Tables does both — define your columns once in TypeScript, and filtering, sorting, pagination, even inline edits become real queries against Postgres, MySQL, or SQLite. The UI is source you copy into your repo and own.

Browse the examplesGitHub

01live demo

~1M rows. Real Postgres.

This table queries a Neon Postgres database (users + profiles) through the Drizzle adapter. Filter by joined profile fields, stack sorts, select rows — and watch the readout below show the exact SQL generated for every interaction. (Postgres · Neon)

Name
Email
Age
Role
Status
Has Bio
Role Tags
Posts
Joined
Bio
Website
Location
GitHub
Jakye Bly
trusting_beverlyn@hotmail.com
20
viewer
inactive
Yes
viewer
0
September 30th, 2023
Nam scelerisque, turpis eget ...--
Jacob Sellman
oxymoronic_dario@hotmail.fr
60
editor
active
Yes
editor
0
November 22nd, 2024
Nulla non dapibus nibh, id ul...https://portfolio.ioBeaumont@hacker
Deondra Dinino
mirthful_vila@live.com
43
editor
active
Yes
editor
0
March 12th, 2025
Praesent cursus lobortis accu...-Abai@techie
Yuli Leggins
mocking_elois@free.fr
60
viewer
pending
Yes
viewer
0
February 6th, 2023
Donec pellentesque feugiat lo...https://example.dev@builder
Erline Mcgarrigle
mocking_niko@rediffmail.com
30
viewer
active
Yes
viewer
0
September 5th, 2025
Maecenas faucibus metus sed i...https://example.devHammamet-
Leasha Taketa
mocking_regena@gmail.com
28
viewer
active
Yes
viewer
0
September 19th, 2024
Nam pellentesque nisi a laore...-Kitahiroshima-
Kawika Galyon
moaning_gaylin@hotmail.com
34
admin
active
Yes
admin
0
August 11th, 2022
Etiam et viverra felis, in vu...-Sido@techie
Jayanthony Sanor
tumultuous_ming@yahoo.co.uk
41
contributor
inactive
Yes
contributor
1
September 12th, 2024
Integer ligula nisl, finibus ...-Mosrah-
Jenette Madole
mocking_raley@web.de
27
viewer
suspended
Yes
viewer
0
December 22nd, 2025
Etiam interdum pellentesque e...-@hacker
Yonatan Croley
posthumous_vianca@web.de
31
viewer
active
Yes
viewer
0
January 13th, 2023
Sed sit amet est eu tortor ef...https://example.dev@builder
Showentries
generated sql — live from this page's server
select "users"."id", "users"."name", "users"."email", "users"."age",
       "users"."role", "users"."status", "users"."created_at",
       "profiles"."id", "profiles"."user_id", "profiles"."bio",
       "profiles"."avatar", "profiles"."website", "profiles"."location",
       "profiles"."github", (select count(*)
from "posts"
where "posts"."user_id" = "users"."id") as "postsCount"
from "users"
left join "profiles" on "users"."id" = "profiles"."user_id"
limit 10
+ 1 more statement (count query)
select count(distinct "users"."id")
from "users"
left join "profiles" on "users"."id" = "profiles"."user_id"

Captured from the drizzle logger during this render. Bound parameters inlined for readability.

02how it works

Columns in. Queries out.

One column definition drives everything. When a user filters or sorts, that action becomes SQL for Postgres, MySQL, or SQLite; the results render straight back into the UI — typed all the way from the database to the cell.

define

Define columns

One builder chain per column. A dotted path like customer.company pulls in a field from a related table — and any typo in that path is a compile error, not a 2 a.m. surprise.

columns.ts
export const tickets = defineTable<Tables>()(
  'tickets',
  (t) => ({
    columns: [
      ...t.auto(), // inferred from schema
      t.text('customer.company') // joined
        .searchable()
        .editable(),
      t.number('reopenCount').filterable(),
      t.date('createdAt').sortable(),
    ],
  })
);

compile

State becomes SQL

Every filter, sort, and page turns into a real query against your database. The adapter joins only the tables that query actually needs — nothing speculative, nothing over-fetched.

what the adapter runs
select "tickets"."id", "tickets"."subject",
       "customers"."company"
from "tickets"
left join "customers"
  on "customers"."id"
     = "tickets"."customer_id"
where "customers"."company" like '%acme%'
order by "tickets"."created_at" desc
limit 10 offset 0

own

Own the UI

The CLI copies the table and filter components into your repo as source you can edit, already wired to your shadcn/ui theme. No black-box dependency to fight later.

terminal
$ npx @better-tables/cli@latest init
# detects your shadcn/ui setup
# installs core + the drizzle adapter
# copies table + filter source into
#   your components dir — yours to edit

04capabilities

The spec sheet.

What ships today — verifiable in the source — and what's next. No adjectives where a fact will do.

Cross-table filtering

shipped

Dot-path columns like customer.company detect Drizzle relations and generate only the JOINs a query needs.

34 filter operators

shipped

Across text, number, date, option, multi-option, boolean, and JSON groups — null-aware throughout.

Editable cells

shipped

Type-aware inline editors with optimistic updates and rollback. Editing a joined column writes the related row.

Schema-inferred columns

shipped

t.auto() reads your database schema; enum columns become dropdowns with humanized labels, no options list needed.

Zero-boilerplate saves

shipped

tables.cellEditAction(def) returns a save handler that's ready to export as a Next.js server action.

URL-synced state

shipped

Filters, sorting, pagination, column visibility, and column order — compressed, shareable, SSR-hydration safe.

Virtualized rows

shipped

Scroll 12,000+ rows with variable heights — see the big board example.

Multi-column sort

shipped

Priority-ordered and reorderable, with a configurable cap.

Typed end to end

shipped

Column paths, filter values, and row types infer from your schema. A renamed column fails the build, not production.

Three databases

shipped

PostgreSQL, MySQL, and SQLite through the Drizzle adapter — plus an HTTP adapter with a ready-made route handler.

Visual query-group builder

planned

The nested AND/OR data model ships today; the drag-to-build UI is on the roadmap.

CSV export

planned

Export the current filtered view from the toolbar.

More adapters

planned

REST and in-memory adapters, for tables without a Drizzle schema.

05install

Own your tables.

One command copies the table and filter source into your repo — components you edit, not a dependency you fight. Requires an existing shadcn/ui setup; the CLI checks, and installs any missing primitives.