Skip to main content
Nexus Design System
GitHub

nxds with Ant Design

Add nxds components to an Ant Design app, and keep antd for everything else. These steps use a Next.js App Router app on antd 6.

What nxds adds for you

nxds gives an antd app parts it doesn't have, themed from one appearance object. Introduction covers the theme engine, the copy-and-own source and the agent support.

You neednxds part
A command paletteCommandDialog
A collapsible app sidebarSidebar and SidebarProvider
A filter barFilterBuilder
A right-click menu or menu barContextMenu and Menubar
A bottom sheetDrawer
A select with several valuesMultiSelect
Large radio or checkbox choicesChoiceCard, ChoiceRow
A chartChartContainer
A chat threadMessage and Bubble

Add your first nxds component

Set up nxds, then add a component:

npx nxds-cli@latest init
npx nxds-cli@latest add button

init adds the provider and the theme script to app/layout.tsx, and creates app/nxds.css. antd/dist/reset.css sets box-sizing. If your app imports it, delete the Preflight lines in app/nxds.css, as Add nxds to an existing app explains. If your app has no reset, keep Preflight.

antd's component styles target only its own .ant- classes, so they need no cascade layer. antd/dist/reset.css does style bare elements, such as p { margin-bottom }. Load it into a layer below the nxds utilities:

/* app/globals.css */
@layer antd-reset, properties, theme, base, components, utilities, nx-runtime;
@import 'antd/dist/reset.css' layer(antd-reset);

Render the nxds button on an existing screen, beside antd's:

// app/page.tsx
import { Button as AntdButton, Flex } from 'antd';

import { Button } from '@/components/nxds/components/button/button';

export default function Page() {
  return (
    <Flex gap="small">
      <AntdButton type="primary">Save</AntdButton>
      <Button variant="outline">Preview</Button>
    </Flex>
  );
}

antd's popups sit at z-index 1000 to 1070. Raise the nxds layers above them, so an nxds popup opens over an antd modal:

/* app/nxds.css, after the imports */
:root {
  --nx-z-index-modal: 1100;
  --nx-z-index-popover: 1200;
  --nx-z-index-toast: 1300;
}

Inside an nxds dialog, render antd popups in place with getPopupContainer={(trigger) => trigger.parentElement ?? document.body}.

Share light and dark mode

nxds owns the mode. antd has no color mode of its own: it picks colors from the algorithm you pass to ConfigProvider. Pick the algorithm from the nxds mode:

// app/antd-provider.tsx
'use client';

import { ConfigProvider, theme } from 'antd';

import { useNxdsAppearance } from '@/components/nxds/components/appearance/provider';

import { brand } from './brand';

export function AntdProvider({ children }: { children: React.ReactNode }) {
  const { resolvedMode } = useNxdsAppearance();

  return (
    <ConfigProvider
      theme={{
        algorithm:
          resolvedMode === 'dark'
            ? theme.darkAlgorithm
            : theme.defaultAlgorithm,
        token: { colorPrimary: brand.color, borderRadius: brand.radius },
      }}
    >
      {children}
    </ConfigProvider>
  );
}

antd renders its styles in React, so it can't read the nxds head script. Read the mode from a cookie in the root layout instead, so the server renders the right algorithm on the first request:

// app/layout.tsx
import { AntdRegistry } from '@ant-design/nextjs-registry';
import {
  createNxdsAppearanceSnapshotFromCookie,
  DEFAULT_COOKIE_KEY,
} from '@nxds/core';
import { cookies } from 'next/headers';

import { NxdsAppearanceProvider } from '@/components/nxds/components/appearance/provider';
import { NxdsAppearanceScript } from '@/components/nxds/components/appearance/provider/server';

import { AntdProvider } from './antd-provider';
import { defaultAppearance } from './appearance-state';

import './globals.css';
import './nxds.css';

export default async function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const cookieStore = await cookies();
  const snapshot = createNxdsAppearanceSnapshotFromCookie(
    cookieStore.get(DEFAULT_COOKIE_KEY)?.value,
    defaultAppearance
  );

  return (
    <html lang="en" suppressHydrationWarning>
      <head>
        <NxdsAppearanceScript
          storageKey="app-appearance"
          defaultState={snapshot.state}
        />
      </head>
      <body>
        <NxdsAppearanceProvider
          storageKey="app-appearance"
          cookieWriteKey={DEFAULT_COOKIE_KEY}
          defaultState={snapshot.state}
        >
          <AntdRegistry>
            <AntdProvider>{children}</AntdProvider>
          </AntdRegistry>
        </NxdsAppearanceProvider>
      </body>
    </html>
  );
}

The provider writes the cookie each time the mode changes. Load the theme from a cookie explains the setup. Reading cookies renders every route on each request.

The server can't see the operating system's mode. When mode is 'system' and the system is dark, antd paints light for a moment, then turns dark after hydration. Set mode to 'light' or 'dark' to avoid it.

Switch the mode through nxds:

// app/mode-toggle.tsx
'use client';

import { Button } from 'antd';

import { useNxdsAppearance } from '@/components/nxds/components/appearance/provider';

export function ModeToggle() {
  const { setState, resolvedMode } = useNxdsAppearance();
  const next = resolvedMode === 'dark' ? 'light' : 'dark';

  return (
    <Button onClick={() => setState((current) => ({ ...current, mode: next }))}>
      Switch to {next}
    </Button>
  );
}

Match your theme

Keep the values both systems read in one file:

// app/brand.ts
export const brand = { color: '#1677ff', radius: 6 };
// app/appearance-state.ts
import { DEFAULT_NXDS_APPEARANCE, type NxdsAppearanceState } from '@nxds/core';

import { brand } from './brand';

export const defaultAppearance = {
  ...DEFAULT_NXDS_APPEARANCE,
  brandColor: brand.color,
  surfaceTone: 'neutral',
  corners: 'smooth',
} satisfies NxdsAppearanceState;
antd tokenAppearance fieldHow to match
colorPrimarybrandColorPass it as is. nxds builds the hover and pressed shades
colorBgBase, colorTextBasesurfaceToneantd's grays are pure, so neutral matches
borderRadiuscorners0 is square, 2 to 4 is subtle, 6 to 8 is smooth, 10 to 12 is round, 16 is extra-round. antd's default is 6
compactAlgorithm, controlHeight, sizeStepdensitycompactAlgorithm matches compact. Compare others in the Playground
boxShadowelevationCompare in the Playground. flat turns shadows off
lineWidthstroke1 is normal, 2 is strong
fontFamilyprefs.uiFontPass the same font stack
fontSizeprefs.uiFontSizeBoth default to 14
darkAlgorithmmodenxds owns it, as Share light and dark mode shows

Where nxds fits

antd components take data props, such as items, options and columns. nxds components take children: one part per item. nxds handlers take the value itself: onValueChange(value), onCheckedChange(checked), onOpenChange(open).

Ant DesignnxdsWhat changes
ButtonButtontype primary, default, dashed, text, link → variant default, outline, dashed, ghost, link
Button dangerButton variant="destructive"
Button with only an iconButton size="icon"Give it an aria-label
Space.Compact with buttonsButtonGroup
Form.ItemField, FieldLabel, FieldDescription, FieldErrorlabel, extra and help become parts. validateStatus="error" → data-invalid
InputInput in a Field
Input with prefix, suffix or addonBeforeInputGroup, InputGroupAddon, InputGroupInput
Input.TextAreaTextarea
Input.OTPInputOTP, InputOTPGroup, InputOTPSlot
CheckboxCheckbox with a FieldLabelonChange(event) → onCheckedChange(checked)
Checkbox.GroupSeveral Checkbox parts in a FieldGroup
Radio.Group, RadioRadioGroup, RadioGroupItemonChange(event) → onValueChange(value)
Radio.Button, SegmentedToggleGroup type="single"onChange → onValueChange
SwitchSwitchonChange(checked) → onCheckedChange(checked)
SelectSelect, SelectTrigger, SelectValue, SelectContent, SelectItemoptions → SelectItem children. onChange → onValueChange(value)
Select mode="multiple"MultiSelectonChange(values) → onValuesChange(values)
AutoComplete, Select showSearchComboboxCompose a ComboboxTrigger, ComboboxInput and ComboboxItem list
SliderSlidervalue is an array, with two values for range. onChange → onValueChange
DatePickerDatePicker in a PopoverTakes React Day Picker props: mode, selected, onSelect
CalendarDatePicker
ModalDialog, DialogContent, DialogHeader, DialogTitle, DialogBody, DialogFooteronCancel → onOpenChange(open). footer → DialogFooter
Modal.confirm, PopconfirmAlertDialog
DrawerSheet, SheetContentplacement → side. onClose → onOpenChange(open)
DropdownDropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItemmenu={{ items }} → DropdownMenuItem children
Menu mode="inline"SidebarMenu, SidebarMenuItem, SidebarMenuButton
Menu mode="horizontal"NavigationMenu
PopoverPopover, PopoverTrigger, PopoverContentcontent → PopoverContent children
TooltipTooltip, TooltipTrigger, TooltipContenttitle → TooltipContent children
message, notificationToaster, toast()message.success('Saved') → toast.success('Saved')
AlertAlert, AlertTitle, AlertDescriptiontype error, warning, info, success → variant destructive, warning, information, success
CollapseAccordion, AccordionItem, AccordionTrigger, AccordionContentitems → AccordionItem children. accordion → type="single"
TabsTabs, TabsList, TabsTrigger, TabsContentitems → children. onChange → onValueChange
BreadcrumbBreadcrumb, BreadcrumbList, BreadcrumbItem, BreadcrumbLinkitems → BreadcrumbItem children
PaginationPagination, PaginationContent, PaginationLink
CardCard, CardHeader, CardTitle, CardAction, CardContent, CardFootertitle → CardTitle, extra → CardAction, actions → CardFooter
Avatar, Avatar.GroupAvatar, AvatarImage, AvatarFallback, AvatarGroup
TagBadgecolor → variant
Tag closableFilterChip
DividerSeparator
ProgressProgressLine progress only
Spin, SkeletonSpinner, Skeleton
TableTable, TableHeader, TableBody, TableRow, TableHead, TableCellRender columns and dataSource as rows yourself
Table rowSelectionTableSelectionHead, TableSelectionCell
DescriptionsDescriptionList, DescriptionListItem, DescriptionListTerm, DescriptionListDescription
Empty, ResultEmptyState
ListItemGroup, Item, ItemTitle, ItemDescription
CarouselCarousel, CarouselContent, CarouselItem
SplitterResizablePanelGroup, ResizablePanel, ResizableHandle
Layout.SiderSidebar
Flex, Space, Row, Col, Layoutnx: flex and grid classesAt the default density, gap small, middle, large → nx:gap-2, nx:gap-4, nx:gap-6
Typography.Title, Typography.Textnx:typography-* classesSuch as nx:typography-heading-large and nx:typography-body-default
Grid.useBreakpointShow and Hide<Hide below="md"> hides content below the md breakpoint

Swap a component (optional)

Swap a component when you touch the screen anyway. This form field moves from antd to nxds:

// Before
import { Form, Input } from 'antd';

<Form.Item
  label="Email"
  extra="We never share it."
  validateStatus={error ? 'error' : undefined}
  help={error}
>
  <Input type="email" />
</Form.Item>;
// After
import {
  Field,
  FieldDescription,
  FieldError,
  FieldLabel,
} from '@/components/nxds/components/field/field';
import { Input } from '@/components/nxds/components/input/input';

<Field data-invalid={Boolean(error)}>
  <FieldLabel htmlFor="email">Email</FieldLabel>
  <Input id="email" type="email" aria-invalid={Boolean(error)} />
  <FieldDescription>We never share it.</FieldDescription>
  <FieldError>{error}</FieldError>
</Field>;

antd's Form validates the fields inside each Form.Item. Move a whole form at once, or validate the nxds fields with your own form library. See Forms.

To check the nx: classes in the files you move over, add the nxds lint rules.

What stays on Ant Design

nxds has no part for these. Keep them on antd:

  • Steps
  • Rate
  • Tree and TreeSelect
  • Cascader
  • Transfer
  • Upload
  • TimePicker
  • InputNumber and Mentions
  • ColorPicker
  • Timeline
  • Tour
  • Statistic, QRCode, Image and Watermark
  • FloatButton
  • Badge, the count on an icon
  • Progress type="circle"
  • Table with sorting, filters or virtual scroll