Skip to main content
Nexus Design System
GitHub

nxds with Chakra UI

Add nxds components to a Chakra UI app, and keep Chakra for everything else. These steps use a Next.js App Router app on Chakra UI 3.

What nxds adds for you

nxds gives a Chakra 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
An app sidebarSidebar and SidebarProvider
A date pickerDatePicker
A chartChartContainer
A filter barFilterBuilder
A select with several valuesMultiSelect
A menu bar or right-click menuMenubar and ContextMenu
A carouselCarousel
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. Chakra's reset sets box-sizing, so delete the Preflight lines in app/nxds.css, as Add nxds to an existing app explains.

Chakra puts its CSS in four cascade layers: reset, base, tokens and recipes. It declares them after nxds loads, so its reset would win over nx: classes and strip an nxds button's padding. Name every layer at the top of globals.css, so Chakra's sit below the nxds utilities:

/* app/globals.css */
@layer properties, theme, reset, base, tokens, recipes, components, utilities, nx-runtime;

Chakra's layers keep their own order, so your Chakra components look the same.

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

// app/page.tsx
import { Button as ChakraButton, HStack } from '@chakra-ui/react';

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

export default function Page() {
  return (
    <HStack gap="2">
      <ChakraButton>Save</ChakraButton>
      <Button variant="outline">Preview</Button>
    </HStack>
  );
}

Chakra's overlays sit at z-index 1300 to 1800. Raise the nxds layers above them, so an nxds popup opens over a Chakra dialog:

/* app/nxds.css, after the imports */
:root {
  --nx-z-index-modal: 1900;
  --nx-z-index-popover: 2000;
  --nx-z-index-toast: 2100;
}

Inside an nxds dialog, render Chakra popups in place: leave out their <Portal>, or pass <Portal disabled>.

On Next.js 16, Chakra asks you to add --webpack to your dev and build scripts: next dev --webpack and next build --webpack. Turbopack hydrates Chakra's styles out of order, which logs a hydration error. See Chakra's Next.js guide.

Share light and dark mode

nxds owns the mode. Chakra reads the .dark class through next-themes, the same class nxds writes. Make next-themes follow nxds in the provider that Chakra's CLI created:

// components/ui/provider.tsx
'use client';

import { ChakraProvider, defaultSystem } from '@chakra-ui/react';
import { ThemeProvider, useTheme } from 'next-themes';
import { useEffect } from 'react';

import { defaultAppearance } from '@/app/appearance-state';
import { useNxdsAppearance } from '@/components/nxds/components/appearance/provider';

function ChakraFollowsNxds() {
  const { state, mounted } = useNxdsAppearance();
  const { setTheme } = useTheme();

  useEffect(() => {
    if (mounted) setTheme(state.mode);
  }, [mounted, state.mode, setTheme]);

  return null;
}

export function Provider({ children }: { children: React.ReactNode }) {
  return (
    <ChakraProvider value={defaultSystem}>
      <ThemeProvider
        attribute="class"
        defaultTheme={defaultAppearance.mode}
        disableTransitionOnChange
      >
        <ChakraFollowsNxds />
        {children}
      </ThemeProvider>
    </ChakraProvider>
  );
}

Put it inside the nxds provider:

// app/layout.tsx
import { NxdsAppearanceProvider } from '@/components/nxds/components/appearance/provider';
import { NxdsAppearanceScript } from '@/components/nxds/components/appearance/provider/server';
import { Provider } from '@/components/ui/provider';

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

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

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en" suppressHydrationWarning>
      <head>
        <NxdsAppearanceScript
          storageKey="app-appearance"
          defaultState={defaultAppearance}
        />
      </head>
      <body>
        <NxdsAppearanceProvider
          storageKey="app-appearance"
          defaultState={defaultAppearance}
        >
          <Provider>{children}</Provider>
        </NxdsAppearanceProvider>
      </body>
    </html>
  );
}

next-themes uses 'light', 'dark' and 'system', like nxds. Its defaultTheme matches the nxds default, so a first visit paints one mode. If next-themes saved another mode before you added nxds, that mode can show for a moment on the first load. From then on, both save the same mode.

next-themes has no option to stop saving its own copy, and it syncs that copy across tabs. A toggle in another tab switches the .dark class here, and the nxds state catches up on the next reload.

Switch the mode through nxds only, not Chakra's setColorMode:

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

import { Button } from '@chakra-ui/react';

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

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

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

useColorMode and useColorModeValue keep working, because they read next-themes.

Match your theme

// app/appearance-state.ts
import { DEFAULT_NXDS_APPEARANCE, type NxdsAppearanceState } from '@nxds/core';

export const defaultAppearance = {
  ...DEFAULT_NXDS_APPEARANCE,
  brandColor: '#2563eb',
  surfaceTone: 'zinc',
  corners: 'subtle',
  prefs: {
    ...DEFAULT_NXDS_APPEARANCE.prefs,
    uiFont: 'Inter, sans-serif',
  },
} satisfies NxdsAppearanceState;
Chakra themeAppearance fieldHow to match
colors.brand, your colorPalettebrandColorUse the shade your solid buttons show. nxds builds the hover and pressed shades
colors.graysurfaceToneChakra's default gray is the same scale as zinc
radii.l2corners0 is square, 0.25rem is subtle, 0.5rem is smooth, 0.75rem is round, 1rem is extra-round. Chakra's default l2 is 0.25rem
spacingdensityCompare in the Playground
shadowselevationCompare in the Playground. flat turns shadows off
NonestrokeChakra has no border width setting, so keep normal
fonts.bodyprefs.uiFontChakra's default is Inter
The color modemodenxds owns it, as Share light and dark mode shows

Where nxds fits

Chakra 3 passes a details object to its handlers, such as onValueChange({ value }). nxds handlers take the value itself: onValueChange(value), onCheckedChange(checked), onOpenChange(open).

Chakra UInxdsWhat changes
ButtonButtonvariant solid, subtle, outline, ghost, plain → default, secondary, outline, ghost, link
Button colorPalette="red"Button variant="destructive"nxds has no colorPalette. The brand comes from the appearance
IconButtonButton size="icon"Give it an aria-label
Group attachedButtonGroup
Field.Root, Field.Label, Field.HelperText, Field.ErrorTextField, FieldLabel, FieldDescription, FieldErrorinvalid → data-invalid on Field and aria-invalid on the input
Field.RequiredIndicatorFieldRequiredIndicator
Fieldset.Root, Fieldset.Legend, Fieldset.ContentFieldSet, FieldLegend, FieldGroup
Input, TextareaInput, Textarea
InputGroupInputGroup, InputGroupAddon, InputGroupInputstartElement and endElement → InputGroupAddon
Checkbox.RootCheckbox with a FieldLabelonCheckedChange(details) → onCheckedChange(checked)
Switch.RootSwitchonCheckedChange(details) → onCheckedChange(checked)
RadioGroup.Root, RadioGroup.ItemRadioGroup, RadioGroupItemonValueChange(details) → onValueChange(value)
CheckboxCard, RadioCardChoiceCard
Select.Root with a collectionSelect, SelectTrigger, SelectValue, SelectContent, SelectItemItems are children, not a collection. value is a string, not an array
Select.Root multipleMultiSelectonValueChange(details) → onValuesChange(values)
NativeSelectNativeSelect, NativeSelectOption
ComboboxComboboxCompose a ComboboxTrigger, ComboboxInput and ComboboxItem list
PinInputInputOTP, InputOTPGroup, InputOTPSlot
SliderSlideronValueChange(details) → onValueChange(value)
SegmentGroupToggleGroup type="single"
Dialog.Root, Dialog.Content, Dialog.Header, Dialog.Body, Dialog.FooterDialog, DialogContent, DialogHeader, DialogBody, DialogFooteronOpenChange(details) → onOpenChange(open)
Dialog asking to confirmAlertDialog
DrawerSheet, or Drawer for a bottom sheetplacement → side on SheetContent
MenuDropdownMenu
Popover, HoverCard, TooltipPopover, HoverCard, TooltipPopover.Trigger → PopoverTrigger, Popover.Content → PopoverContent
Toaster, createToasterToaster, toast()Render <Toaster /> once, then call toast('Saved')
Alert.Root, Alert.Title, Alert.DescriptionAlert, AlertTitle, AlertDescriptionstatus error, warning, info, success → variant destructive, warning, information, success
Accordion.Root, Accordion.Item, Accordion.ItemTrigger, Accordion.ItemContentAccordion, AccordionItem, AccordionTrigger, AccordionContentmultiple → type="multiple"
CollapsibleCollapsible, CollapsibleTrigger, CollapsibleContent
Tabs.Root, Tabs.List, Tabs.Trigger, Tabs.ContentTabs, TabsList, TabsTrigger, TabsContentonValueChange(details) → onValueChange(value)
BreadcrumbBreadcrumb, BreadcrumbList, BreadcrumbItem, BreadcrumbLink
PaginationPagination, PaginationContent, PaginationLink
Card.Root, Card.Header, Card.Body, Card.FooterCard, CardHeader, CardContent, CardFooterCard.Title → CardTitle, Card.Description → CardDescription
Avatar, AvatarGroupAvatar, AvatarImage, AvatarFallback, AvatarGroup
BadgeBadgecolorPalette → variant. variant="outline" → fill="outline"
Tag with a close buttonFilterChip
SeparatorSeparator
ProgressProgress
Spinner, SkeletonSpinner, Skeleton
Table.Root, Table.Header, Table.Body, Table.Row, Table.ColumnHeader, Table.CellTable, TableHeader, TableBody, TableRow, TableHead, TableCell
EmptyStateEmptyState, EmptyStateHeader, EmptyStateTitle, EmptyStateDescription
DataListDescriptionList, DescriptionListItem, DescriptionListTerm, DescriptionListDescription
Kbd, ScrollAreaKbd, ScrollArea
SplitterResizablePanelGroup, ResizablePanel, ResizableHandle
Box, Flex, Stack, Grid, Containernx: flex and grid classesAt the default density, gap="4" → nx:gap-4 and p="4" → nx:p-4
Text, Headingnx:typography-* classesSuch as nx:typography-heading-large and nx:typography-body-default
hideBelow, hideFrom, useBreakpointValueShow and HidehideBelow="md" → <Hide below="md">

Swap a component (optional)

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

// Before
import { Field, Input } from '@chakra-ui/react';

<Field.Root invalid={Boolean(error)}>
  <Field.Label>Email</Field.Label>
  <Input type="email" />
  <Field.HelperText>We never share it.</Field.HelperText>
  <Field.ErrorText>{error}</Field.ErrorText>
</Field.Root>;
// 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>;

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

What stays on Chakra UI

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

  • Steps
  • RatingGroup
  • ColorPicker and ColorSwatch
  • FileUpload
  • Editable
  • NumberInput
  • PasswordInput
  • ProgressCircle
  • Stat
  • Timeline
  • TreeView
  • QrCode, Clipboard, Marquee and ActionBar