Ant Design Essentials
Ant Design Essentials Ant Design (antd) is a comprehensive React UI library built around a design language originally created for enterprise back-office tools a…
Ant Design Essentials
Ant Design (antd) is a comprehensive React UI library built around a design language originally created for enterprise back-office tools at Alibaba. Its value is breadth and consistency: forms, tables, date pickers, modals, and layout primitives all share the same design tokens and interaction patterns out of the box, so you spend less time reinventing components and more time wiring up business logic. It leans opinionated and dense (good for admin dashboards, internal tools, data-heavy apps) rather than minimal or highly custom-branded consumer UI — for that, you usually reach for the theming system to pull it toward your brand instead of fighting the defaults.
Setup & Theming with ConfigProvider
antd v5 dropped the old less-based theming for a JS-in-CSS token system (CSS-in-JS under the hood, powered by @ant-design/cssinjs). Wrap your app in ConfigProvider once, near the root, and every descendant component picks up the theme automatically — no separate CSS build step, no importing a compiled theme.css.
import { ConfigProvider, theme as antdTheme } from 'antd'
import type { ThemeConfig } from 'antd'
const customTheme: ThemeConfig = {
token: {
colorPrimary: '#5b3df0',
colorSuccess: '#16a34a',
borderRadius: 8,
fontFamily: "'Inter', -apple-system, sans-serif",
},
components: {
Button: {
controlHeight: 40,
fontWeight: 500,
},
Table: {
headerBg: '#f8f7ff',
rowHoverBg: '#f3f1ff',
},
},
// Swap the whole algorithm for dark mode instead of overriding colors by hand
algorithm: antdTheme.defaultAlgorithm,
}
export function App({ children }: { children: React.ReactNode }) {
return (
<ConfigProvider theme={customTheme} componentSize="middle">
{children}
</ConfigProvider>
)
}
// Dark mode toggle — swap the algorithm, tokens adapt automatically
function ThemedApp({ dark, children }: { dark: boolean; children: React.ReactNode }) {
return (
<ConfigProvider
theme={{
...customTheme,
algorithm: dark ? antdTheme.darkAlgorithm : antdTheme.defaultAlgorithm,
}}
>
{children}
</ConfigProvider>
)
}`token` sets global design tokens (color, radius, spacing, typography) that cascade to every component. `components` lets you override tokens for a single component type without touching the rest of the theme — much safer than writing custom CSS overrides that fight antd's specificity. Nested ConfigProvider instances merge with (not replace) the parent theme, which is handy for a themed section inside an otherwise default-themed app.
Forms with Form.Item and useForm
antd's Form is uncontrolled by default — Form.Item registers a field with the form instance via `name`, and Form owns the values internally. This avoids the boilerplate of onChange + useState per field, but it means you read/set values through the form instance (`form.getFieldsValue()`, `form.setFieldsValue()`), not through your own component state.
import { Form, Input, Select, Button, InputNumber, message } from 'antd'
type ProjectFormValues = {
name: string
stack: string
budget: number
}
export function ProjectForm({ onCreated }: { onCreated: (v: ProjectFormValues) => void }) {
const [form] = Form.useForm<ProjectFormValues>()
const onFinish = (values: ProjectFormValues) => {
onCreated(values)
message.success(`Created "${values.name}"`)
form.resetFields()
}
return (
<Form
form={form}
layout="vertical"
onFinish={onFinish}
initialValues={{ stack: 'react', budget: 5000 }}
>
<Form.Item
label="Project name"
name="name"
rules={[
{ required: true, message: 'Please enter a project name' },
{ min: 3, message: 'At least 3 characters' },
]}
>
<Input placeholder="e.g. Internal dashboard" />
</Form.Item>
<Form.Item label="Stack" name="stack" rules={[{ required: true }]}>
<Select
options={[
{ value: 'react', label: 'React' },
{ value: 'vue', label: 'Vue' },
{ value: 'nextjs', label: 'Next.js' },
]}
/>
</Form.Item>
{/* dependency-driven field: revalidates when 'stack' changes */}
<Form.Item
label="Budget (USD)"
name="budget"
dependencies={['stack']}
rules={[
{ required: true },
({ getFieldValue }) => ({
validator(_, value) {
if (getFieldValue('stack') === 'nextjs' && value < 2000) {
return Promise.reject('Next.js projects need a budget of at least $2000')
}
return Promise.resolve()
},
}),
]}
>
<InputNumber min={0} step={100} style={{ width: '100%' }} />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
Create project
</Button>
</Form.Item>
</Form>
)
}Async/cross-field validation goes through a `validator` function that returns a Promise — reject with a message string to fail, resolve to pass. `dependencies` tells that Form.Item to re-run validation whenever a listed field changes, which matters because antd only re-validates a field when its own value changes by default.
Table: Columns, Sorting, Server-Side Pagination
Table is data-driven: you describe columns declaratively and pass a `dataSource` array. For anything beyond a few hundred rows, move sorting/filtering/pagination server-side and drive them off the `onChange` callback instead of letting the Table sort the whole client-side array.
import { Table, Tag } from 'antd'
import type { TableProps } from 'antd'
import { useState } from 'react'
type Repo = {
id: string
name: string
language: string
stars: number
status: 'active' | 'archived'
}
const columns: TableProps<Repo>['columns'] = [
{ title: 'Repository', dataIndex: 'name', key: 'name' },
{
title: 'Language',
dataIndex: 'language',
key: 'language',
filters: [
{ text: 'TypeScript', value: 'TypeScript' },
{ text: 'Python', value: 'Python' },
],
onFilter: (value, record) => record.language === value,
},
{
title: 'Stars',
dataIndex: 'stars',
key: 'stars',
sorter: (a, b) => a.stars - b.stars,
defaultSortOrder: 'descend',
},
{
title: 'Status',
dataIndex: 'status',
key: 'status',
render: (status: Repo['status']) => (
<Tag color={status === 'active' ? 'green' : 'default'}>{status.toUpperCase()}</Tag>
),
},
]
export function RepoTable() {
const [data, setData] = useState<Repo[]>([])
const [loading, setLoading] = useState(false)
const [pagination, setPagination] = useState({ current: 1, pageSize: 20, total: 0 })
const fetchPage = async (page: number, pageSize: number) => {
setLoading(true)
const res = await fetch(`/api/repos?page=${page}&limit=${pageSize}`)
const json = await res.json()
setData(json.data)
setPagination({ current: page, pageSize, total: json.total })
setLoading(false)
}
return (
<Table<Repo>
rowKey="id"
columns={columns}
dataSource={data}
loading={loading}
pagination={pagination}
onChange={(pag) => fetchPage(pag.current ?? 1, pag.pageSize ?? 20)}
/>
)
}Layout Primitives: Layout, Row/Col, Space
antd ships its own 24-column grid (`Row`/`Col`) predating widespread CSS Grid/Flexbox adoption, plus `Layout` for page chrome (header/sider/content) and `Space` for consistent gaps between inline elements — reach for `Space` before hand-rolling flex + margin utility classes.
import { Layout, Row, Col, Space, Button, Menu } from 'antd'
const { Header, Sider, Content } = Layout
export function DashboardShell() {
return (
<Layout style={{ minHeight: '100vh' }}>
<Sider breakpoint="lg" collapsedWidth="0">
<Menu
theme="dark"
mode="inline"
items={[
{ key: 'projects', label: 'Projects' },
{ key: 'settings', label: 'Settings' },
]}
/>
</Sider>
<Layout>
<Header style={{ display: 'flex', justifyContent: 'space-between' }}>
<Space>
<Button type="text">Home</Button>
<Button type="text">Docs</Button>
</Space>
</Header>
<Content style={{ padding: 24 }}>
{/* responsive grid: stacks below md, 3 columns from md up */}
<Row gutter={[16, 16]}>
<Col xs={24} md={8}>
Card A
</Col>
<Col xs={24} md={8}>
Card B
</Col>
<Col xs={24} md={8}>
Card C
</Col>
</Row>
</Content>
</Layout>
</Layout>
)
}Common Gotchas
Form values live in the form instance, not React state — reading a field via component state instead of `form.getFieldValue()`/watching with `Form.useWatch()` gets you stale values.
CSS-in-JS + Next.js App Router needs the style registry — without wrapping the root in antd's `StyleProvider`/`AntdRegistry` for SSR, styles can flash unstyled or duplicate between server and client.
Overriding styles with raw CSS instead of tokens — antd's CSS-in-JS has high specificity; prefer `theme.components` overrides or the `styles`/`classNames` props over `!important` battles.
Client-side sorting/filtering `Table` with large datasets — fine under a few hundred rows; beyond that, pass server-computed `dataSource` and drive sort/filter/pagination off `onChange`.
Importing the whole library — `import { Button } from 'antd'` is already tree-shakeable in v5 with modern bundlers; you don't need the old babel-plugin-import workaround from v4.
Forgetting `rowKey` on Table — without a stable `rowKey`, antd falls back to array index, which breaks row selection and animations when data reorders.