Muhammad Zulfan Wahyudin
Back to Blog

Building a Production-Ready Frontend with Ant Design 6

· 9 min read
  • react
  • ant-design
  • design-system
  • typescript
  • frontend

A practical guide to setting up Ant Design 6 with React, TypeScript, and Vite — covering the new CSS variables architecture, semantic DOM, theming, forms, tables, and bundle optimization.

Why Ant Design 6

Ant Design 6 (released November 2025) is a major technical upgrade over v5. It drops IE support, raises the minimum React version to 18, and shifts to a pure CSS Variables architecture. The result is faster runtime performance, smaller bundles, and a cleaner developer experience.

Key improvements over v5:

  • Pure CSS Variables mode@ant-design/cssinjs defaults to CSS Variables instead of inline styles. This means lighter theme switching, multi-theme reuse, and zero-runtime style extraction.
  • React Compiler ready — the dist output is compiled with the React Compiler for better re-render performance.
  • Semantic DOM for all components — every component now exposes classNames and styles via ConfigProvider, making customization predictable.
  • Semantic structure — all components use logical position props (start / end) with built-in RTL support.
  • New components — Masonry, InputNumber spinner mode, Drawer resizing, Tooltip panning, and mask blur backgrounds.
  • v4 deprecated APIs removed — cleaner API surface with no legacy baggage.
  • Requires React 18+ — no more polyfills for older React versions.

Project Scaffolding

Start with Vite and React:

pnpm create vite my-app --template react-ts
cd my-app
pnpm add antd @ant-design/icons

Make sure your package.json requires React 18 or higher:

{
  "dependencies": {
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "antd": "^6.0.0",
    "@ant-design/icons": "^6.0.0"
  }
}

@ant-design/icons v6 is not compatible with antd v5 — upgrade both together.

Enable the Vite import optimization so Ant Design components are pre-bundled:

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  optimizeDeps: {
    include: ['antd', '@ant-design/icons'],
  },
});

Theme Configuration with CSS Variables

Ant Design 6 uses CSS Variables by default. The ConfigProvider theme API is similar to v5 but now generates CSS Variables under the hood instead of inline styles:

// theme/index.tsx
import { ConfigProvider, theme } from 'antd';

type ThemeMode = 'light' | 'dark';

export function AppTheme({ children }: { children: React.ReactNode }) {
  const [mode, setMode] = useState<ThemeMode>('light');

  const toggleMode = () =>
    setMode((prev) => (prev === 'light' ? 'dark' : 'light'));

  return (
    <ConfigProvider
      theme={{
        algorithm: mode === 'dark' ? theme.darkAlgorithm : theme.defaultAlgorithm,
        token: {
          colorPrimary: '#1677ff',
          borderRadius: 8,
          fontFamily: "'Inter', system-ui, sans-serif",
        },
        components: {
          Button: {
            controlHeightLG: 48,
            fontWeight: 600,
          },
          Table: {
            headerBg: mode === 'dark' ? '#141414' : '#fafafa',
          },
        },
      }}
    >
      <ThemeContext.Provider value={{ mode, toggleMode }}>
        {children}
      </ThemeContext.Provider>
    </ConfigProvider>
  );
}

The key difference in v6: all tokens become real CSS Variables. You can inspect them in DevTools under --ant-* prefixes. This makes runtime theme switching instant — no style recalculation, just variable swaps.

Semantic classNames and styles

New in v6: every component accepts classNames and styles through ConfigProvider for predictable customization:

<ConfigProvider
  button={{
    classNames: {
      icon: 'custom-icon',
    },
    styles: {
      icon: { fontSize: 18 },
    },
  }}
>
  <Button type="primary" icon={<SearchOutlined />}>Search</Button>
</ConfigProvider>

This replaces the old pattern of overriding internal DOM classes with hardcoded selectors.

Layout Architecture

A production layout needs a sidebar, header, and content area. Ant Design provides Layout, Sider, Header, and Content components:

// components/AppLayout.tsx
import { Layout, Menu } from 'antd';
import {
  DashboardOutlined,
  SettingOutlined,
  UserOutlined,
} from '@ant-design/icons';
import { Outlet, useNavigate, useLocation } from 'react-router-dom';

const menuItems = [
  { key: '/dashboard', icon: <DashboardOutlined />, label: 'Dashboard' },
  { key: '/users', icon: <UserOutlined />, label: 'Users' },
  { key: '/settings', icon: <SettingOutlined />, label: 'Settings' },
];

export function AppLayout() {
  const navigate = useNavigate();
  const location = useLocation();

  return (
    <Layout style={{ minHeight: '100vh' }}>
      <Sider collapsible breakpoint="lg" width={240}>
        <div className="logo" />
        <Menu
          mode="inline"
          selectedKeys={[location.pathname]}
          items={menuItems}
          onClick={({ key }) => navigate(key)}
        />
      </Sider>
      <Layout>
        <Header className="px-6 flex items-center justify-end" />
        <Content className="p-6">
          <Outlet />
        </Content>
      </Layout>
    </Layout>
  );
}

Key points:

  • Sider with collapsible and breakpoint="lg" gives you a responsive sidebar for free.
  • Outlet works with React Router v6 for nested routing.
  • The selectedKeys prop on Menu syncs with the current route.

Form Best Practices

Ant Design’s Form component is powerful but easy to misuse. Here’s a production setup:

import { Form, Input, Select, Button, message } from 'antd';

type UserForm = {
  name: string;
  email: string;
  role: 'admin' | 'editor' | 'viewer';
};

export function UserForm() {
  const [form] = Form.useForm<UserForm>();

  const onFinish = async (values: UserForm) => {
    try {
      await api.createUser(values);
      message.success('User created');
      form.resetFields();
    } catch {
      message.error('Failed to create user');
    }
  };

  return (
    <Form<UserForm>
      form={form}
      layout="vertical"
      onFinish={onFinish}
      initialValues={{ role: 'viewer' }}
      scrollToFirstError
    >
      <Form.Item
        name="name"
        label="Name"
        rules={[{ required: true, message: 'Name is required' }]}
      >
        <Input placeholder="John Doe" />
      </Form.Item>

      <Form.Item
        name="email"
        label="Email"
        rules={[
          { required: true, message: 'Email is required' },
          { type: 'email', message: 'Enter a valid email' },
        ]}
      >
        <Input placeholder="john@example.com" />
      </Form.Item>

      <Form.Item name="role" label="Role" rules={[{ required: true }]}>
        <Select
          options={[
            { value: 'admin', label: 'Admin' },
            { value: 'editor', label: 'Editor' },
            { value: 'viewer', label: 'Viewer' },
          ]}
        />
      </Form.Item>

      <Form.Item>
        <Button type="primary" htmlType="submit" block size="large">
          Create User
        </Button>
      </Form.Item>
    </Form>
  );
}

Rules to follow:

  • Always type your form with Form<T>() — TypeScript catches field name typos.
  • Use layout="vertical" for better readability on long forms.
  • scrollToFirstError is a small UX win that makes a big difference on validation failure.
  • Co-locate validation rules with the field definition, not in a separate schema.

Table Configuration

Tables are where most apps spend their UI budget. A well-configured Ant Design Table handles pagination, sorting, and filtering declaratively:

import { Table, Tag } from 'antd';
import type { ColumnsType } from 'antd/es/table';

type User = {
  id: string;
  name: string;
  email: string;
  status: 'active' | 'inactive';
  role: string;
  createdAt: string;
};

const columns: ColumnsType<User> = [
  {
    title: 'Name',
    dataIndex: 'name',
    sorter: (a, b) => a.name.localeCompare(b.name),
  },
  {
    title: 'Email',
    dataIndex: 'email',
  },
  {
    title: 'Status',
    dataIndex: 'status',
    render: (status: User['status']) => (
      <Tag color={status === 'active' ? 'green' : 'red'}>
        {status.toUpperCase()}
      </Tag>
    ),
    filters: [
      { text: 'Active', value: 'active' },
      { text: 'Inactive', value: 'inactive' },
    ],
    onFilter: (value, record) => record.status === value,
  },
  {
    title: 'Role',
    dataIndex: 'role',
  },
  {
    title: 'Created',
    dataIndex: 'createdAt',
    render: (date: string) => new Date(date).toLocaleDateString(),
    defaultSortOrder: 'descend',
    sorter: (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(),
  },
];

export function UserTable({ data }: { data: User[] }) {
  return (
    <Table<User>
      columns={columns}
      dataSource={data}
      rowKey="id"
      pagination={{
        pageSize: 10,
        showSizeChanger: true,
        showTotal: (total) => `Total ${total} users`,
      }}
    />
  );
}

In v6, you can customize internal table elements via classNames and styles through ConfigProvider:

<ConfigProvider
  table={{
    classNames: {
      header: 'custom-table-header',
      row: 'custom-table-row',
    },
    styles: {
      header: { background: 'var(--color-bg-elevated)' },
    },
  }}
>
  <Table {...props} />
</ConfigProvider>

Internationalization

Ant Design supports 60+ locales out of the box. Wrap your app with the locale config:

import { ConfigProvider } from 'antd';
import enUS from 'antd/locale/en_US';
import idID from 'antd/locale/id_ID';

export function I18nProvider({ children }: { children: React.ReactNode }) {
  const [locale, setLocale] = useState(enUS);

  return (
    <ConfigProvider locale={locale}>
      <LocaleContext.Provider
        value={{
          locale,
          setLocale,
          toggle: () => setLocale(locale === enUS ? idID : enUS),
        }}
      >
        {children}
      </LocaleContext.Provider>
    </ConfigProvider>
  );
}

This localizes all Ant Design components — pagination labels, date picker months, modal buttons — without any manual translation.

Bundle Optimization

Ant Design is large. A default import of everything adds significant weight to your bundle. Here’s how to keep it lean in v6:

Tree-Shaking (Automatic with ESM)

Ant Design 6 ships ESM modules. With Vite, tree-shaking works out of the box:

// Good — tree-shakeable
import { Button, Table } from 'antd';

// Bad — imports everything
// import antd from 'antd';

Day.js (Still the Default)

Ant Design 6 continues using Day.js (~6 KB) instead of Moment.js. Verify no Moment.js leaks into your bundle:

pnpm add source-map-explorer
npx source-map-explorer dist/assets/*.js

If you see moment, remove it:

pnpm remove moment

Code Splitting Routes

Lazy-load pages that use Ant Design components:

import { lazy, Suspense } from 'react';
import { Spin } from 'antd';

const Dashboard = lazy(() => import('./pages/Dashboard'));
const UserList = lazy(() => import('./pages/UserList'));

function App() {
  return (
    <Suspense fallback={<Spin className="flex justify-center mt-20" />}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/users" element={<UserList />} />
      </Routes>
    </Suspense>
  );
}

Zero-Runtime Style Extraction

New in v6: you can extract Ant Design styles at build time using @ant-design/static-style-extract. This eliminates runtime CSS injection entirely:

pnpm add -D @ant-design/static-style-extract
// scripts/extract-styles.ts
import { extractStyle } from '@ant-design/static-style-extract';
import fs from 'fs';

const css = extractStyle();
fs.writeFileSync('public/antd.min.css', css);

Then import the generated CSS in your entry point:

import '/public/antd.min.css';

This is optional but recommended for apps with strict performance budgets.

New in v6: Masonry Component

Ant Design 6 introduces a Masonry component for grid layouts:

import { Masonry } from 'antd';

const items = [
  { key: '1', height: 200, content: 'Card A' },
  { key: '2', height: 300, content: 'Card B' },
  { key: '3', height: 150, content: 'Card C' },
];

export function Gallery() {
  return (
    <Masonry columns={3} gap={16}>
      {items.map((item) => (
        <div
          key={item.key}
          style={{ height: item.height, background: '#f0f0f0' }}
        >
          {item.content}
        </div>
      ))}
    </Masonry>
  );
}

Summary

Ant Design 6 is a significant step forward for React UI at scale. The CSS Variables architecture makes theming faster and more predictable. The semantic DOM structure gives you clean customization hooks through classNames and styles. Combined with the new Masonry component and React Compiler support, v6 is the most performant version yet.

The framework is worth your time if you’re building internal tools, dashboards, or any app where consistency and development speed matter more than pixel-perfect custom design.

Have a project in mind? Let's build something together.

Get in Touch