DEV Community

Peter
Peter

Posted on

How to Build a React Datatable Like Linear in 2026

A few years ago, if you asked “what React table should I use?”, the conversation was mostly about rendering rows, sorting, maybe filtering, and how customizable the grid was.

In 2026, that’s not really the hard part anymore.

The hard part is building a datatable like Linear: something that already feels like part of a serious product.

I mean the stuff around the grid itself:

  • filters
  • saved views
  • URL state
  • bulk actions
  • grouping
  • persistence
  • server-side loading
  • shareable state
  • keyboard-friendly workflows
  • a UI that feels coherent instead of stitched together

I know this because I ended up building the same thing 4 times across different React projects.

And every time, the pattern was the same.

The grid itself was only a small part of the work.

The real engineering time disappeared into all the surrounding UX and behavior that users start expecting once the table becomes part of a real workflow.

So eventually I stopped rebuilding it from scratch and turned it into a product: react-datatable.

What most React table libraries give you

Most table libraries sit at one of two ends:

1. Headless flexibility

This is great when you want total control.

But it also means you are still responsible for assembling the full table experience yourself:

  • toolbar behavior
  • filter UX
  • saved views
  • URL sync
  • selection state
  • bulk action flows
  • persistence
  • online query contracts
  • empty/loading/error states
  • all the UI glue between those pieces

2. Fully built grid products

These can be powerful, but often come with tradeoffs:

  • the code does not really become yours
  • customization can feel constrained
  • the UX can feel like “a grid inside your app” instead of “part of your product”
  • agent-driven changes are harder when the core behavior lives outside your codebase

That middle ground is what I wanted.

What I actually wanted: a datatable like Linear

When I say “a datatable like Linear”, I do not mean copying Linear’s exact UI.

I mean the product shape.

A table that already behaves like something users can live in.

That usually means:

  • fast search
  • useful filters
  • views worth saving
  • URL-backed state
  • bulk actions
  • consistent keyboard behavior
  • support for larger datasets
  • a UX that feels deliberate rather than improvised

That’s the gap I built react-datatable for.

The core idea behind react-datatable

react-datatable is a source-owned React datatable.

That part matters.

Instead of giving you a black-box component you configure from the outside, it gives you code you can actually own, copy into your app, and adapt like part of your own product.

That makes it feel much closer to the appeal of shadcn:

  • local source
  • easy to inspect
  • easy to modify
  • easy to extend
  • no weird boundary between “their component” and “your app”

So the goal is not just “yet another React grid”.

The goal is:

a production-shaped React datatable with source ownership.

Why source ownership matters more in 2026

This matters even more now because a lot of frontend work is increasingly done with coding agents.

Agents are great when they can work inside a codebase with clear seams and local ownership.

They are much less useful when the most important product behavior is trapped behind an external abstraction you can only poke through props.

With source-owned components, agents can:

  • modify table UI directly
  • add app-specific columns and actions
  • change saved-view behavior
  • extend row previews
  • adapt bulk actions to backend workflows
  • refactor state flows without fighting a black box

That’s a very different developer experience.

And honestly, I think this becomes a serious advantage in 2026.

A basic local example

Here is a stripped-down local-mode example:

import { Datatable, type DatatableColumn } from "./react-datatable"

type Customer = {
  id: string
  company: string
  status: string
  seats: number
}

const columns: DatatableColumn<Customer>[] = [
  {
    id: "company",
    accessorKey: "company",
    header: "Company",
    filterType: "text",
  },
  {
    id: "status",
    accessorKey: "status",
    header: "Status",
    filterType: "select",
  },
  {
    id: "seats",
    accessorKey: "seats",
    header: "Seats",
    filterType: "number",
  },
]

export function CustomersTable({ rows }: { rows: Customer[] }) {
  return (
    <Datatable
      columns={columns}
      data={rows}
      getRowId={(row) => row.id}
      toolbar={{
        quickSearch: true,
        filterButton: true,
        displayOptions: true,
      }}
      selection={{ enabled: true }}
      virtualization={{ mode: "viewport", estimateRowHeight: 44, overscan: 8 }}
    />
  )
}
Enter fullscreen mode Exit fullscreen mode

That already gets you much closer to a real product table than just “render some rows”.

A more realistic example

This is where the shape starts to matter more.

import { Datatable, type DatatableColumn, type DataTableBulkAction } from "./react-datatable"

type Customer = {
  id: string
  name: string
  company: string
  status: "Active" | "Trial" | "Paused"
  plan: "Starter" | "Team" | "Enterprise"
  seats: number
  revenue: number
}

const columns: DatatableColumn<Customer>[] = [
  {
    id: "name",
    header: "Name",
    accessorKey: "name",
    enableSorting: true,
    enableFiltering: true,
    filterType: "text",
  },
  {
    id: "status",
    header: "Status",
    accessorKey: "status",
    enableSorting: true,
    enableFiltering: true,
    enableGrouping: true,
    filterType: "text-list",
    filterOptions: {
      options: [
        { label: "Active", value: "Active" },
        { label: "Trial", value: "Trial" },
        { label: "Paused", value: "Paused" },
      ],
    },
  },
  {
    id: "revenue",
    header: "Revenue",
    accessorKey: "revenue",
    enableSorting: true,
    enableFiltering: true,
    filterType: "number",
    cell: ({ getValue }) => `$${Number(getValue()).toLocaleString()}`,
  },
]

const bulkActions: DataTableBulkAction<Customer>[] = [
  {
    id: "export",
    title: "Export selected customers",
    keywords: ["csv", "download"],
    onSelect: (context) => {
      console.log(`Export ${context.selectedCount} customers`)
      context.clearSelection()
      context.closeDialog()
    },
  },
]

export function RevenueTable({ rows }: { rows: Customer[] }) {
  return (
    <Datatable
      data={rows}
      columns={columns}
      getRowId={(row) => row.id}
      toolbar={{
        quickSearch: { placeholder: "Search customers..." },
        filterButton: true,
        displayOptions: true,
        copyLink: false,
        views: false,
      }}
      initialState={{
        sorting: [{ id: "name", desc: false }],
      }}
      stickyColumnsCount={1}
      selection={{
        enabled: true,
        mode: "multi",
        allowSelectAllMatching: true,
      }}
      bulkActions={{
        triggerLabel: "Actions",
        actions: bulkActions,
      }}
      virtualization={{
        mode: "viewport",
        rowOverscanCount: 10,
        columnOverscanCount: 2,
      }}
    />
  )
}
Enter fullscreen mode Exit fullscreen mode

This is the kind of surface I mean.

Not just “a table”.

A workflow surface.

Online mode matters too

The other place teams lose a lot of time is when the table outgrows local-only assumptions.

What starts as a simple client-side table often needs to become:

server-filtered
server-sorted
paginated
groupable
shareable
stable under larger datasets

That transition is where a lot of earlier choices break down.

Here’s what online mode looks like:

import { Datatable } from "./react-datatable"

<Datatable
  columns={columns}
  getRowId={(row) => row.id}
  online={{
    mode: "pagination",
    queryKey: ["customers"],
    pageSize: 50,
    supportedGroupingColumns: ["status"],
    query: (input) =>
      fetch("/api/customers/table", {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify(input),
      }).then((response) => response.json()),
  }}
/>
Enter fullscreen mode Exit fullscreen mode

That matters because the product should not need a full rewrite just because the dataset got real.

Why I think this is the best React datatable framework for 2026

Obviously I’m biased — I built it.

But I also built this kind of thing repeatedly before I productized it, and that’s exactly why I think the framing is different.

I think the best React datatable framework for 2026 should do all of these well:

Feel production-ready

  • not just rows and columns
  • but filters, views, persistence, bulk actions, and state flows

Be source-owned

  • closer to shadcn than to a locked box
  • easy to adapt inside a real app

Scale from local to server-backed

  • without forcing a redesign

Work well with coding agents

  • local code
  • clear seams
  • understandable contracts
  • modifiable UI

Save real developer time

  • not 20 minutes in setup
  • but weeks or months of UX integration work

That’s the bar I was trying to hit with react-datatable.

If your goal is “datatable like Linear”, this is the actual checklistIf that phrase brought you here, I’d use this checklist:

  • Can I own the source?
  • Can I change the UI deeply?
  • Can I support saved views and URL state?
  • Can I handle bulk actions cleanly?
  • Can I move from local mode to online mode?
  • Can I work on it effectively with coding agents?
  • Does it feel like part of my product instead of a bolted-on grid?

That’s a much better decision framework than just comparing feature matrices.

Final thought

After building the same React datatable shape 4 times, I stopped believing the hard part was “finding a grid”.

The hard part is building the surrounding product behavior without losing months to glue code and edge cases.

That’s why I built react-datatable.

If you’re trying to build a datatable like Linear in React — with source ownership, strong UX, and code that agents can actually help you extend — that’s exactly what it’s for.

Docs:
https://react-datatable.com/docs

Top comments (0)