docs/content/recipes/themes/ant-design/ant-design.md
In this tutorial, you will integrate Handsontable into a React app that uses Ant Design and align the grid with your design system tokens through the Theme API. You will learn how to map Ant Design tokens to Handsontable theme parameters so the grid matches your existing styles.
<iframe src="https://codesandbox.io/embed/n3c25x?view=preview&module=%2Fsrc%2FApp.tsx" style="width:100%; height: 500px; border:0; border-radius: 4px; overflow:hidden;" title="Handsontable with AntDesign" allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking" sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts" ></iframe>Open in CodeSandbox View source on GitHub
This recipe uses the official handsontable/examples Ant Design demo as the live reference implementation. It shows how to integrate Handsontable into a React app that uses Ant Design by registering a custom theme and mapping Handsontable theme colors and tokens to Ant Design table-like styles.
Difficulty: Beginner
Time: ~15 minutes
Stack: React, Ant Design, Handsontable, @handsontable/react-wrapper
registerTheme('ant-data-grid', { icons, colors, tokens }).colors/ant) with token-based overrides.handsontable and @handsontable/react-wrapper installed in your app.In your project root:
npm install handsontable @handsontable/react-wrapper antd
(or npm install / yarn add).
Mirror the official example setup by registering modules, preparing row data, and creating a reusable theme instance.
import { registerAllModules } from 'handsontable/registry';
import { getTheme, hasTheme, horizonTheme, registerTheme } from 'handsontable/themes';
import colorsAnt from 'handsontable/themes/static/variables/colors/ant';
registerAllModules();
const data = [
{
key: '1',
name: 'John Brown',
age: 32,
address: 'New York No. 1 Lake Park',
tags: ['nice', 'developer'],
actionHint: null,
},
{
key: '2',
name: 'Jim Green',
age: 42,
address: 'London No. 1 Lake Park',
tags: ['loser'],
actionHint: null,
},
{
key: '3',
name: 'Joe Black',
age: 32,
address: 'Sydney No. 1 Lake Park',
tags: ['cool', 'teacher'],
actionHint: null,
},
];
const THEME_NAME = 'horizon-ant-table';
const antTableTheme = (() => {
if (hasTheme(THEME_NAME)) {
return getTheme(THEME_NAME);
}
return registerTheme(THEME_NAME, horizonTheme)
.params({
colors: colorsAnt,
tokens: {
borderRadius: '8px',
headerBackgroundColor: ['colors.palette.100', 'colors.palette.800'],
headerFontWeight: '600',
cellHorizontalBorderColor: ['colors.palette.200', 'colors.palette.700'],
cellVerticalBorderColor: ['colors.palette.200', 'colors.palette.700'],
cellHorizontalPadding: '16px',
cellVerticalPadding: '8px',
rowCellEvenBackgroundColor: ['colors.white', 'colors.palette.950'],
rowCellOddBackgroundColor: ['colors.white', 'colors.palette.950'],
cellReadOnlyBackgroundColor: ['colors.white', 'colors.palette.950'],
foregroundColor: ['colors.palette.800', 'colors.palette.100'],
linkColor: ['colors.primary.200', 'colors.primary.100'],
linkHoverColor: ['colors.primary.100', 'colors.primary.200'],
},
})
.setColorScheme('light')
.setDensityType('comfortable');
})();
Use Ant Design typography, tags, and spacing inside custom renderer components.
import { Space, Tag, Typography } from 'antd';
function NameCell({ value }) {
const label = value != null ? String(value) : '';
return (
<Typography.Link href="#" onClick={(event) => event.preventDefault()}>
{label}
</Typography.Link>
);
}
function TagsCell({ value }) {
const tags = Array.isArray(value) ? value : [];
if (tags.length === 0) {
return null;
}
return (
<Space size={4} wrap>
{tags.map((tag) => {
let color = tag.length > 5 ? 'geekblue' : 'green';
if (tag === 'loser') {
color = 'volcano';
}
return (
<Tag key={tag} color={color}>
{String(tag).toUpperCase()}
</Tag>
);
})}
</Space>
);
}
function ActionCell({ instance, row }) {
const rowData = instance.getSourceDataAtRow(row);
return (
<Space size="middle">
<Typography.Link href="#" onClick={(event) => event.preventDefault()}>
Invite {rowData?.name ?? ''}
</Typography.Link>
<Typography.Link href="#" onClick={(event) => event.preventDefault()}>
Delete
</Typography.Link>
</Space>
);
}
Build the Ant-like table layout with HotTable, fixed column widths, and read-only cells.
import { useCallback } from 'react';
import { HotTable, HotColumn } from '@handsontable/react-wrapper';
function AntLikeGrid() {
const readOnlyCell = useCallback(() => ({ readOnly: true }), []);
return (
<HotTable
theme={antTableTheme}
data={data}
colHeaders={['Name', 'Age', 'Address', 'Tags', 'Action']}
rowHeaders={false}
stretchH="all"
height="auto"
autoRowSize={false}
licenseKey="non-commercial-and-evaluation"
cells={readOnlyCell}
>
<HotColumn data="name" width={160} renderer={NameCell} />
<HotColumn data="age" width={72} type="numeric" />
<HotColumn data="address" width={280} />
<HotColumn data="tags" width={240} renderer={TagsCell} />
<HotColumn data="actionHint" width={220} renderer={ActionCell} />
</HotTable>
);
}
CardRender the final view with Ant Design card styling around the table component.
import { Card, Typography } from 'antd';
export default function App() {
return (
<div style={{ padding: 24, background: '#f5f5f5', minHeight: '100vh' }}>
<Card title={<Typography.Text strong>Handsontable with Ant Design theme tokens</Typography.Text>}>
<AntLikeGrid />
</Card>
</div>
);
}
colors/ant, then override only the values you need.ConfigProvider.theme.getDesignToken() and map them to Handsontable Theme API parameters.hot.updateSettings({ theme: new HotTableTheme({ ... }) }).ConfigProvider from Ant Design to drive both the Ant Design components and the Handsontable theme from a single source of truth.