Building a data table from scratch is one of those tasks that always looks small and never is. You start with a list of rows, then someone wants to sort by a column. Then filter. Then edit a cell in place. Then export to CSV. Then group rows, freeze a header, paginate the results, and make it all responsive on mobile. Before long your "simple table" is a small application of its own.
Tabulator is the library that absorbs all of that for you. You hand it a DOM element and a configuration object, and it builds a fully interactive table out of an existing HTML <table>, a JavaScript array, or a JSON feed. Sorting, filtering, inline editing, pagination, grouping, and export are all built in. It is written in plain JavaScript with zero runtime dependencies, ships under the MIT license, and works equally well in React, Vue, Angular, or no framework at all. With roughly 700,000 downloads a month and a codebase that still sees commits weekly, it has become one of the go-to choices when you want a real data grid without signing up for an enterprise license.
Why Reach for It
The data grid space is crowded, so it helps to know what Tabulator actually optimizes for.
- Batteries included, rendering and all. Unlike headless table libraries that hand you logic and leave the markup to you, Tabulator renders the table itself. Formatters, editors, sorters, and export buttons are configuration, not components you build.
- Three data sources, one API. Progressive-enhance an existing HTML table, pass an array of objects, or point it at a remote JSON endpoint with built-in remote pagination, sorting, and filtering.
- Zero dependencies, MIT license. There is no paid tier and no commercial license to negotiate. The entire feature set is free and open source.
- Framework agnostic. Because it manages its own DOM, the same instance works inside React, Vue, Angular, or a static page.
- Performance for large datasets. A virtual DOM renderer keeps thousands of rows scrolling smoothly.
- A deep feature set. Inline editors with validation, multi-column sorting, per-header filters, row grouping, data trees for hierarchical rows, frozen columns and headers, drag-to-reorder rows, column calculations, and export to CSV, JSON, XLSX, PDF, or the clipboard.
If you have ever weighed AG Grid (powerful but with a paid Enterprise tier) against Handsontable (great editing but a commercial license for commercial use) against TanStack Table (headless, so you write all the markup), Tabulator sits in a comfortable middle: it does the rendering for you and asks nothing in return.
Getting It Into Your Project
Install the package from npm or yarn:
npm install tabulator-tables
yarn add tabulator-tables
Tabulator ships its styling separately, so you import a theme stylesheet alongside the library. There are several themes in the box (the default, plus midnight, modern, and variants for Bootstrap, Materialize, Semantic UI, and Bulma):
import { TabulatorFull as Tabulator } from "tabulator-tables";
import "tabulator-tables/dist/css/tabulator.min.css";
If you only need a slice of the features, the version 6 build is fully tree-shakeable, so you can import the core Tabulator class plus only the modules you use instead of the all-in-one TabulatorFull. For a quick prototype or a static page, the UNPKG CDN works too, with one <link> for the CSS and one <script> for the library.
Your First Table
The core pattern never changes: create a container element, then turn it into a table with a configuration object.
import { TabulatorFull as Tabulator } from "tabulator-tables";
import "tabulator-tables/dist/css/tabulator.min.css";
interface Person {
id: number;
name: string;
age: number;
rating: number;
active: boolean;
}
const data: Person[] = [
{ id: 1, name: "Ada Lovelace", age: 36, rating: 5, active: true },
{ id: 2, name: "Alan Turing", age: 41, rating: 4, active: false },
{ id: 3, name: "Grace Hopper", age: 85, rating: 5, active: true },
];
const table = new Tabulator("#example-table", {
data,
layout: "fitColumns",
columns: [
{ title: "Name", field: "name" },
{ title: "Age", field: "age", hozAlign: "right", sorter: "number" },
{ title: "Active", field: "active", formatter: "tickCross" },
],
});
The columns array is where most of the work happens. Each entry maps a title (the header label) to a field (the property on your row object) and then layers on behavior. The layout: "fitColumns" option stretches the columns to fill the available width; other modes like fitData size each column to its content instead. That is genuinely all it takes to get a clean, scrollable, sortable table on screen.
Loading Data From Anywhere
The data option handles a local array, but Tabulator is just as happy fetching from a server. Point it at a URL and it will request the data, and with the right options it will hand off pagination, sorting, and filtering to the backend so the browser never holds the entire dataset at once.
const table = new Tabulator("#example-table", {
ajaxURL: "/api/people",
pagination: true,
paginationMode: "remote",
paginationSize: 25,
sortMode: "remote",
filterMode: "remote",
columns: [
{ title: "Name", field: "name" },
{ title: "Age", field: "age", sorter: "number" },
],
});
You can also swap the data out at any time after construction with table.setData(newArrayOrUrl), which is the method you will lean on when wiring Tabulator into a framework. The same table instance keeps all its state and just re-renders with the fresh rows.
Formatters That Make Cells Readable
Raw values are rarely what you want to show a user. Formatters transform a cell's value into rendered HTML, and Tabulator ships a generous library of them. A boolean becomes a tick or cross, a number becomes a star rating, a URL becomes a clickable link, and a number can render as a progress bar. When the built-ins are not enough, a formatter can be a function that returns whatever HTML you like.
const columns = [
{ title: "Name", field: "name" },
{
title: "Rating",
field: "rating",
formatter: "star",
formatterParams: { stars: 5 },
},
{
title: "Progress",
field: "completion",
formatter: "progress",
formatterParams: { min: 0, max: 100, color: ["red", "orange", "green"] },
},
{
title: "Profile",
field: "url",
formatter: "link",
formatterParams: { label: "View", target: "_blank" },
},
];
Because formatters only affect display, the underlying data stays clean. Sorting still works on the real value, and exports use the raw value rather than the rendered HTML unless you tell them otherwise.
Where It Earns Its Keep
The starter examples are pleasant, but Tabulator is built for the moment a table needs to become a workspace rather than a read-only list.
Editing Cells In Place
Set an editor on a column and that cell becomes directly editable when a user clicks it. The editor library mirrors the formatter library: text inputs, numbers, dropdown lists, dates, checkboxes, star ratings, and more. Pair an editor with a validator and Tabulator will refuse invalid input before it ever reaches your data.
const table = new Tabulator("#example-table", {
data,
columns: [
{
title: "Name",
field: "name",
editor: "input",
validator: ["required", "string"],
},
{
title: "Age",
field: "age",
editor: "number",
validator: ["required", "min:0", "max:120"],
},
{
title: "Role",
field: "role",
editor: "list",
editorParams: { values: ["Engineer", "Designer", "Manager"] },
},
],
});
table.on("cellEdited", (cell) => {
console.log(`${cell.getField()} changed to`, cell.getValue());
});
The cellEdited event is your hook back into application logic: persist the change, fire off an API call, or update some other part of the UI. Tabulator has a rich event bus beyond this, including rowClick, dataLoaded, and tableBuilt, so you can react to almost anything the table does.
Grouping, Trees, and Calculations
Once a dataset gets large, flat rows stop being enough. Tabulator can group rows under collapsible headers, render hierarchical data as expandable trees, and compute running calculations in dedicated header or footer rows.
const table = new Tabulator("#example-table", {
data: salesData,
groupBy: "region",
columns: [
{ title: "Region", field: "region" },
{ title: "Rep", field: "rep" },
{
title: "Revenue",
field: "revenue",
sorter: "number",
bottomCalc: "sum",
bottomCalcFormatter: "money",
},
],
});
Here groupBy collapses the rows into per-region sections, while bottomCalc: "sum" adds a footer row that totals the revenue column and keeps the total in sync as data changes. For nested data, the data tree feature lets a row contain child rows that expand and collapse, and recent releases sharpened how child rows roll up into those column calculations. These are the features that separate a table that displays data from one people actually use to make decisions.
Living Inside React
Because Tabulator owns its own DOM, the cleanest way to use it in React is to let it do exactly that. Create a ref to a container, instantiate the table once inside an effect, and tear it down on unmount. The key discipline is to never let React recreate the table on every render. Instantiate once, then push updates through Tabulator's own methods like setData and setColumns.
import { useEffect, useRef } from "react";
import { TabulatorFull as Tabulator } from "tabulator-tables";
import "tabulator-tables/dist/css/tabulator.min.css";
export function PeopleTable({ data }: { data: Person[] }) {
const containerRef = useRef<HTMLDivElement>(null);
const tableRef = useRef<Tabulator | null>(null);
useEffect(() => {
if (!containerRef.current) return;
tableRef.current = new Tabulator(containerRef.current, {
data,
layout: "fitColumns",
columns: [
{ title: "Name", field: "name", editor: "input" },
{ title: "Age", field: "age", sorter: "number", hozAlign: "right" },
],
});
return () => {
tableRef.current?.destroy();
};
}, []);
useEffect(() => {
tableRef.current?.setData(data);
}, [data]);
return <div ref={containerRef} />;
}
The first effect builds the table once and registers a cleanup that calls destroy() so nothing leaks. The second effect watches your data prop and feeds new rows to the existing instance. There is a community react-tabulator wrapper that packages this pattern into a <ReactTabulator /> component, but it tends to lag the core release cadence, so a thin custom wrapper like this keeps you on the latest tabulator-tables with no middleman.
The Takeaway
Tabulator is the rare data grid that asks very little of you up front and still scales to genuinely demanding tables. The configuration-driven API means a sortable, filterable, editable table is a matter of describing your columns rather than assembling components. Formatters and editors cover the common cases out of the box, grouping and data trees handle structured datasets, and built-in export saves you from reaching for another library when someone inevitably asks for a CSV.
What makes it especially easy to recommend is what it does not cost: no enterprise tier, no commercial license, no dependency tree to audit. It is plain JavaScript under an MIT license that happily slots into whatever framework you already use. If your next feature involves showing people a lot of data and letting them do something with it, Tabulator is a strong first stop before you commit to anything heavier.