Mantine DataTable in React: Complete Setup, Tutorial & Examples (2025)
Published: June 2025 · 12 min read · React · Mantine UI · Data Tables
So you need a data table in React. You’ve already looked at TanStack Table, weighed the overhead of AG Grid, and briefly considered just building your own — then immediately thought better of it. Enter mantine-datatable: a feature-rich, TypeScript-first React data table component built on top of the Mantine UI ecosystem. It gives you sorting, pagination, row selection, and server-side data fetching without the usual ceremony — and it looks good doing it.
This guide walks you through everything from installation to advanced usage patterns. Whether you’re exploring mantine-datatable for the first time or migrating from another React data table library, you’ll find actionable code and honest assessments of where the library shines — and where it asks for a bit of patience.
What Is mantine-datatable and Why Should You Care?
mantine-datatable is an open-source React data grid component authored by Ionuț-Cristian Florescu and distributed under the MIT licence. It wraps Mantine’s native Table primitive and extends it with production-grade features: multi-column sorting, row click handlers, custom cell renderers, sticky headers, column pinning, infinite scrolling, and server-side pagination — all exposed through a clean, declarative props API. The library is TypeScript-native, meaning autocomplete actually works and your column definitions stay type-safe without extra gymnastics.
The key distinction from generic solutions like TanStack Table (formerly React Table) is philosophy. TanStack Table is headless — you bring your own markup and styles. Mantine DataTable is opinionated in the best way: it renders a fully styled, accessible table out of the box, inheriting your MantineProvider theme automatically. If your project already uses Mantine UI, integrating mantine-datatable is less «library adoption» and more «installing a component you should have had all along.»
As of 2025 the library tracks Mantine v7+ and ships with first-class support for the new CSS-variables theming system. Community adoption has grown steadily — the npm package consistently clears 100k monthly downloads — which is a reasonable signal that the maintenance burden is spread across an engaged contributor base rather than resting on a single overworked author. That matters more than any benchmark when you’re choosing a React table with Mantine for a long-lived project.
mantine-datatable Installation: Getting the Environment Right
Before a single line of table code, your project needs Mantine’s peer dependencies in place. If you’re starting fresh, the fastest path is Mantine’s official Vite or Next.js template; if you’re adding to an existing app, a few targeted installs is all it takes. The mantine-datatable setup process is deliberately minimal — the library avoids pulling in extra bundler plugins or PostCSS chains beyond what Mantine itself requires.
Run the following in your project root:
# npm
npm install mantine-datatable @mantine/core @mantine/hooks
# yarn
yarn add mantine-datatable @mantine/core @mantine/hooks
# pnpm
pnpm add mantine-datatable @mantine/core @mantine/hooks
Mantine v7 dropped the Emotion dependency in favour of plain CSS. You need to import Mantine’s base stylesheet and mantine-datatable’s own stylesheet — typically at your app entry point (main.tsx or _app.tsx):
import '@mantine/core/styles.css';
import 'mantine-datatable/styles.css';
Then wrap your component tree with MantineProvider. This is the single configuration step that connects mantine-datatable to your design system — colours, radius, font scale, dark mode — so that the table inherits every theme token automatically rather than requiring a separate config object.
import { MantineProvider } from '@mantine/core';
export default function App() {
return (
<MantineProvider>
{/* your routes / components */}
</MantineProvider>
);
}
That’s genuinely the entire setup. No Webpack alias, no Babel plugin, no peer-dependency conflict archaeology. If you’ve spent any time fighting ag-grid’s module system, the relief is palpable.
mantine-datatable Basic Usage: Your First Working Table
The DataTable component accepts two mandatory props: columns and records. Columns is an array of column descriptor objects; records is your data array. Everything else — pagination, selection, sorting — is opt-in. This is the mantine-datatable basic usage pattern that covers 80% of real-world cases without any ceremony.
import { DataTable } from 'mantine-datatable';
const employees = [
{ id: 1, name: 'Alice Chen', role: 'Frontend Engineer', department: 'Product' },
{ id: 2, name: 'Bob Okafor', role: 'Backend Engineer', department: 'Platform' },
{ id: 3, name: 'Clara Müller', role: 'Product Designer', department: 'Design' },
];
export function EmployeeTable() {
return (
<DataTable
withTableBorder
borderRadius="sm"
highlightOnHover
records={employees}
columns={[
{ accessor: 'name', title: 'Full Name' },
{ accessor: 'role', title: 'Role' },
{ accessor: 'department', title: 'Department' },
]}
/>
);
}
The accessor field maps directly to a key in your data objects. TypeScript will infer the record type from the records prop and flag mismatched accessor strings at compile time — a small thing that saves real debugging time as column counts grow. highlightOnHover and withTableBorder are cosmetic props that ship enabled in most production UIs; they’re one-liners rather than CSS overrides, which is exactly the kind of ergonomic shortcut a component library should provide.
Custom cell rendering deserves a mention here because it’s where most «basic» tables actually live. The render function on a column descriptor receives the full row record and can return any React node. You can drop in a Mantine Badge, a Menu, an action button, or an avatar without touching the table’s layout logic:
import { Badge } from '@mantine/core';
{
accessor: 'department',
title: 'Department',
render: ({ department }) => (
<Badge color={department === 'Design' ? 'grape' : 'blue'} variant="light">
{department}
</Badge>
),
}
This composability is the practical advantage of staying within the Mantine ecosystem. You’re not wrestling with the table’s internal DOM to inject a styled component — you’re just returning JSX. The Mantine UI table renders it correctly because DataTable and Badge share the same design token layer.
Sorting, Pagination & Row Selection Without the Headache
Enabling sorting on a column takes exactly two things: marking the column as sortable: true and maintaining a sortStatus state object. The library handles the visual indicator (the sort arrow), the ARIA attributes, and keyboard navigation. You handle the actual data sorting — either client-side with a simple sort function, or by forwarding the sort parameters to your API for server-side ordering. The separation is intentional and sane.
import { useState } from 'react';
import { DataTable, DataTableSortStatus } from 'mantine-datatable';
import sortBy from 'lodash/sortBy';
export function SortableTable() {
const [sortStatus, setSortStatus] = useState<DataTableSortStatus>({
columnAccessor: 'name',
direction: 'asc',
});
const sorted = sortBy(employees, sortStatus.columnAccessor);
const records = sortStatus.direction === 'desc' ? sorted.reverse() : sorted;
return (
<DataTable
records={records}
sortStatus={sortStatus}
onSortStatusChange={setSortStatus}
columns={[
{ accessor: 'name', title: 'Name', sortable: true },
{ accessor: 'role', title: 'Role', sortable: true },
{ accessor: 'department', title: 'Department', sortable: true },
]}
/>
);
}
Pagination follows the same pattern. You provide page, onPageChange, totalRecords, and recordsPerPage. The component renders a fully accessible paginator — page numbers, prev/next buttons, optional records-per-page selector — and calls your handler when the user navigates. For server-side pagination, you just fire a new API request inside onPageChange and update your records state. The table doesn’t need to know whether the data came from a local array or a GraphQL endpoint.
Row selection is equally direct. Pass selectedRecords and onSelectedRecordsChange to get checkbox-based multi-selection with shift-click range support and select-all built in. A common pattern is to derive an action bar visibility from selectedRecords.length > 0 — delete, export, bulk-edit — which pairs naturally with Mantine’s Transition component for a smooth show/hide animation. Building the same from scratch with a headless table library is a solid afternoon of work; here it’s fifteen minutes including the action bar.
Advanced mantine-datatable Patterns: Server-Side Data & Nested Rows
Real applications rarely work with static arrays. The most common advanced scenario is server-side data fetching: the table shows a loading state while data is in flight, updates on sort or page change, and handles errors gracefully. mantine-datatable exposes a fetching boolean prop that overlays a progress bar on the table header and optionally disables interactions during loading — no custom overlay component required.
import { useEffect, useState } from 'react';
import { DataTable } from 'mantine-datatable';
const PAGE_SIZE = 15;
export function ServerSideTable() {
const [page, setPage] = useState(1);
const [records, setRecords] = useState([]);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(false);
useEffect(() => {
setLoading(true);
fetch(`/api/employees?page=${page}&limit=${PAGE_SIZE}`)
.then(r => r.json())
.then(({ data, total }) => { setRecords(data); setTotal(total); })
.finally(() => setLoading(false));
}, [page]);
return (
<DataTable
fetching={loading}
records={records}
totalRecords={total}
recordsPerPage={PAGE_SIZE}
page={page}
onPageChange={setPage}
columns={[
{ accessor: 'name', title: 'Name' },
{ accessor: 'role', title: 'Role' },
{ accessor: 'department', title: 'Department' },
]}
/>
);
}
Expandable rows — where clicking a row reveals a detail panel beneath it — are handled through the rowExpansion prop. You define a content render function that receives the record and returns JSX; the library manages open/close state, animation, and ARIA expansion attributes. This is exactly the kind of interaction that trips people up in headless table libraries because the animation layer has to coordinate with the DOM row order. mantine-datatable abstracts all of that.
Column pinning (sticky columns on horizontal scroll), custom row styles via rowStyle and rowClassName callbacks, context menus on right-click, and drag-to-reorder columns are all supported as of the 7.x release cycle. The library’s changelog is worth skimming before you write any custom logic — there’s a reasonable chance the feature you’re about to implement is already a prop away. That said, highly specialised requirements like virtualised rendering for 100k+ rows are not the library’s strength; for those edge cases, TanStack Virtual or AG Grid Enterprise is the more appropriate tool.
Theming and Visual Customisation
Because mantine-datatable is built on top of Mantine’s component system, every visual attribute — colour scheme, border radius, font size, spacing — responds to your MantineProvider theme without any additional configuration. Dark mode works automatically: set colorScheme="dark" on MantineProvider and the table adapts. You don’t need to hunt for CSS custom property overrides or maintain a separate dark-mode stylesheet. This is one of the concrete, day-to-day advantages of choosing a React data table library that lives inside a coherent design system rather than operating standalone.
Fine-grained per-row and per-cell styling is available through the rowStyle callback (receives the record and row index, returns a CSSProperties object), the rowClassName callback (returns a class name string), and the column-level cellsStyle prop. A typical use case is conditionally colouring rows based on a status field — overdue invoices in amber, critical alerts in red — without coupling display logic to your data layer. Keep these callbacks pure and cheap; they run on every render.
For teams running a custom design system that isn’t Mantine’s default theme, the CSS variable approach introduced in Mantine v7 makes overrides surgical. You’re targeting --mantine-color-* tokens at the root level, and the table picks them up automatically. The alternative — manually patching every Mantine component’s className — is not something you want to inherit as tech debt, which makes the token system genuinely important beyond marketing copy.
mantine-datatable vs Other React Data Table Libraries
Comparing React data grid libraries is an exercise in clarifying requirements rather than declaring a winner. mantine-datatable sits in a distinct position: it’s not headless like TanStack Table, not enterprise-feature-complete like AG Grid, and not as opinionated about data-fetching conventions as React Query’s integrations. It is the right choice when your project already uses Mantine UI, your table complexity sits in the «standard CRUD interface» range, and your team values developer experience over raw configurability.
Here’s an honest feature comparison across the most commonly evaluated options:
- mantine-datatable — Mantine-native styling, great DX, full TypeScript, MIT licence. Best for Mantine UI projects.
- TanStack Table v8 — Headless, framework-agnostic, maximum flexibility, steeper learning curve. Best when you need full control over markup.
- AG Grid Community — Extremely feature-rich, virtualisation for massive datasets, complex licensing. Best for enterprise dashboards with 50k+ rows.
- React Data Grid (adazzle) — Excel-like cell editing, good virtualisation, smaller community. Best for spreadsheet-style interfaces.
- MUI DataGrid — Material UI equivalent of mantine-datatable. Best if your stack is MUI rather than Mantine.
The verdict for React interactive table use cases inside Mantine projects is straightforward: mantine-datatable is the default choice, not a compromise. The only time to look elsewhere is when your data volume exceeds ~10k rows without pagination (virtualisation territory), or when you need cell-level editing that mimics a spreadsheet grid. For everything else — dashboards, admin panels, data browsing interfaces, reporting tools — it handles the workload cleanly.
A Real-World mantine-datatable Example: Admin User Table
Theory lands better when it’s grounded in a concrete scenario. The following example represents a typical admin user-management table: server-side pagination, sortable columns, row selection with a bulk-action bar, and a status badge rendered in a custom cell. This is the kind of component that appears in virtually every SaaS dashboard and that mantine-datatable handles without requiring a single external plugin.
import { useState, useEffect } from 'react';
import { DataTable, DataTableSortStatus } from 'mantine-datatable';
import { Badge, Button, Group, Text } from '@mantine/core';
import { IconTrash } from '@tabler/icons-react';
import sortBy from 'lodash/sortBy';
type User = {
id: number;
name: string;
email: string;
status: 'active' | 'inactive' | 'suspended';
role: string;
};
const MOCK_USERS: User[] = [
{ id: 1, name: 'Alice Chen', email: 'alice@acme.com', status: 'active', role: 'Admin' },
{ id: 2, name: 'Bob Okafor', email: 'bob@acme.com', status: 'inactive', role: 'Editor' },
{ id: 3, name: 'Clara Müller', email: 'clara@acme.com', status: 'suspended', role: 'Viewer' },
{ id: 4, name: 'Dana Patel', email: 'dana@acme.com', status: 'active', role: 'Editor' },
{ id: 5, name: 'Elan Bright', email: 'elan@acme.com', status: 'active', role: 'Admin' },
];
const STATUS_COLOUR: Record<User['status'], string> = {
active: 'green',
inactive: 'gray',
suspended: 'red',
};
const PAGE_SIZE = 3;
export function UserManagementTable() {
const [page, setPage] = useState(1);
const [sortStatus, setSortStatus] = useState<DataTableSortStatus>({ columnAccessor: 'name', direction: 'asc' });
const [selectedRecords, setSelected] = useState<User[]>([]);
const sorted = sortBy(MOCK_USERS, sortStatus.columnAccessor);
const ordered = sortStatus.direction === 'desc' ? sorted.reverse() : sorted;
const records = ordered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
return (
<>
{selectedRecords.length > 0 && (
<Group mb="sm">
<Text size="sm">{selectedRecords.length} user(s) selected</Text>
<Button
size="xs"
color="red"
leftSection={<IconTrash size={14} />}
onClick={() => alert(`Deleting ${selectedRecords.map(u => u.name).join(', ')}`)}
>
Delete selected
</Button>
</Group>
)}
<DataTable<User>
withTableBorder
borderRadius="md"
highlightOnHover
records={records}
totalRecords={MOCK_USERS.length}
recordsPerPage={PAGE_SIZE}
page={page}
onPageChange={setPage}
sortStatus={sortStatus}
onSortStatusChange={setSortStatus}
selectedRecords={selectedRecords}
onSelectedRecordsChange={setSelected}
columns={[
{ accessor: 'name', title: 'Name', sortable: true },
{ accessor: 'email', title: 'Email', sortable: true },
{ accessor: 'role', title: 'Role' },
{
accessor: 'status',
title: 'Status',
render: ({ status }) => (
<Badge color={STATUS_COLOUR[status]} variant="light">
{status}
</Badge>
),
},
]}
/>
</>
);
}
This 70-line component delivers pagination, sorting, multi-selection, bulk deletion, and custom status rendering. The same feature set from scratch — even with TanStack Table’s excellent utilities — would be two to three times the code and require wiring up accessible pagination markup manually. The difference isn’t about library capability; it’s about where the development time goes. With mantine-datatable, it goes into product logic rather than table scaffolding.
Worth noting: the TypeScript generic on DataTable<User> makes the render function’s parameter fully typed. Rename a field in the User interface and every column accessor that references it will produce a compile error immediately. This might seem like a minor quality-of-life feature until you’re maintaining a table that’s grown to 25 columns across three files — at that point it’s the difference between a confident refactor and a test-suite treasure hunt.
mantine-datatable Getting Started: Quick Reference Checklist
If you’re integrating mantine-datatable into an existing project today, the following sequence covers every step without backtracking. Consider this the condensed version of everything above — a checklist you can execute in under thirty minutes on a real codebase.
- Install
mantine-datatable,@mantine/core, and@mantine/hooksvia your package manager. - Import
@mantine/core/styles.cssandmantine-datatable/styles.cssat your application entry point. - Ensure
MantineProviderwraps the component tree whereDataTablewill be used. - Define your record type in TypeScript and pass it as a generic to
DataTable<YourType>. - Start with
recordsandcolumnsonly; addsortStatus,page, andselectedRecordsas requirements emerge. - Use
fetchingprop for any asynchronous data source to communicate loading state automatically. - Consult the official documentation for less common props — the docs site includes live interactive examples for every feature.
One last practical note: pin the mantine-datatable version in your package.json to match your Mantine core version. The library follows Mantine’s major releases closely, and a version mismatch between mantine-datatable and @mantine/core is the single most common source of «why is the table unstyled?» bug reports in community channels. Keeping them in sync — both at 7.x or both at 6.x — eliminates the issue entirely.
For a deeper walkthrough with additional code examples and community-contributed patterns, see the original getting-started guide on dev.to/stackforgetx.
Frequently Asked Questions
Run
npm install mantine-datatable @mantine/core @mantine/hooks, then import both @mantine/core/styles.css and mantine-datatable/styles.css at your app entry point. Wrap your component tree with MantineProvider and you’re ready to use DataTable. The full setup takes under five minutes.
Yes — all three are supported out of the box via declarative props. Sorting is enabled per column with
sortable: true; pagination is controlled through page, onPageChange, and totalRecords; filtering requires managing filter state yourself but the library integrates cleanly with any React state solution. All features work with both client-side arrays and server-side API responses.
Yes. mantine-datatable is released under the MIT licence and is free for personal and commercial use. The source code is available on GitHub, and the project accepts community contributions. There is no paid tier or enterprise licence required for any feature in the library.
Semantic Keyword Index (Editorial Reference)
| Cluster | Keywords & LSI Phrases | Intent |
|---|---|---|
| Core Product | mantine-datatable, mantine datatable, DataTable component Mantine | Navigational |
| Stack Context | Mantine DataTable React, React table with Mantine, Mantine UI table | Informational |
| Setup & Install | mantine-datatable installation, mantine-datatable setup, getting started mantine-datatable | Transactional |
| Learning & Tutorial | mantine-datatable tutorial, mantine-datatable example, mantine-datatable basic usage, mantine-datatable getting started | Informational |
| Generic Table | React data table Mantine, React table component, React data table library, React interactive table | Informational |
| Grid / Advanced | React data grid, React data grid component, sortable data table React, paginated table React | Informational |
| LSI / Semantic | TypeScript data table, Mantine v7 table, server-side pagination React, row selection React table, custom cell renderer React | Supporting |
| Comparative | mantine-datatable vs TanStack Table, best React table library 2025, AG Grid alternative React | Commercial |