Build faster with Premium Chakra UI Components 💎

Learn more
Skip to Content
DocsPlaygroundGuidesBlog
Sponsor

Migration to v3

How to migrate to Chakra UI v3.x from v2.x

warning
We recommend using the LLMs.txt files to make the Chakra UI v3 documentation available to large language models.

Steps

The minimum node version required is Node.20.x

1

Update Packages

Remove the unused packages: @emotion/styled and framer-motion. These packages are no longer required in Chakra UI.

npm uninstall @emotion/styled framer-motion

Install updated versions of the packages: @chakra-ui/react and @emotion/react.

npm install @chakra-ui/react@latest @emotion/react@latest

Next, install component snippets using the CLI snippets. Snippets provide pre-built compositions of Chakra components to save you time and put you in charge.

npx @chakra-ui/cli snippet add
2

Refactor Custom Theme

Move your custom theme to a dedicated theme.js or theme.ts file. Use createSystem and defaultConfig to configure your theme.

Before

import { extendTheme } from "@chakra-ui/react"

export const theme = extendTheme({
  fonts: {
    heading: `'Figtree', sans-serif`,
    body: `'Figtree', sans-serif`,
  },
})

After

import { createSystem, defaultConfig } from "@chakra-ui/react"

export const system = createSystem(defaultConfig, {
  theme: {
    tokens: {
      fonts: {
        heading: { value: `'Figtree', sans-serif` },
        body: { value: `'Figtree', sans-serif` },
      },
    },
  },
})

All token values need to be wrapped in an object with a value key. Learn more about tokens here.

3

Update ChakraProvider

Update the ChakraProvider import from @chakra-ui/react to the one from the snippets. Next, rename the theme prop to value to match the new system-based theming approach.

Before

import { ChakraProvider } from "@chakra-ui/react"

export const App = ({ Component }) => (
  <ChakraProvider theme={theme}>
    <Component />
  </ChakraProvider>
)

After

import { Provider } from "@/components/ui/provider"
import { defaultSystem } from "@chakra-ui/react"

export const App = ({ Component }) => (
  <Provider>
    <Component />
  </Provider>
)
import { ColorModeProvider } from "@/components/ui/color-mode"
import { ChakraProvider, defaultSystem } from "@chakra-ui/react"

export function Provider(props) {
  return (
    <ChakraProvider value={defaultSystem}>
      <ColorModeProvider {...props} />
    </ChakraProvider>
  )
}

If you have a custom theme, replace defaultSystem with the custom system

The Provider component compose the ChakraProvider from Chakra and ThemeProvider from next-themes

Improvements

  • Performance: Improved reconciliation performance by 4x and re-render performance by 1.6x

  • Namespaced imports: Import components using the dot notation for more concise imports

    import { Accordion } from "@chakra-ui/react"
    
    const Demo = () => {
      return (
        <Accordion.Root>
          <Accordion.Item>
            <Accordion.ItemTrigger />
            <Accordion.ItemContent />
          </Accordion.Item>
        </Accordion.Root>
      )
    }
  • TypeScript: Improved IntelliSense and type inference for style props and tokens.

  • Polymorphism: Loosened the as prop typings in favor of using the asChild prop. This pattern was inspired by Radix Primitives and Ark UI.

Removed Features

Color Mode

  • ColorModeProvider and useColorMode have been removed in favor of next-themes
  • LightMode, DarkMode and ColorModeScript components have been removed. You now have to use className="light" or className="dark" to force themes.
  • useColorModeValue has been removed in favor of useTheme from next-themes
note
We provide snippets for color mode via the CLI to help you set up color mode quickly using next-themes

Hooks

We removed the hooks package in favor of using dedicated, robust libraries like react-use and usehooks-ts

The only hooks we ship now are useBreakpointValue, useCallbackRef, useDisclosure, useControllableState and useMediaQuery.

Style Config

We removed the styleConfig and multiStyleConfig concept in favor of recipes and slot recipes. This pattern was inspired by Panda CSS.

Next.js package

We've removed the @chakra-ui/next-js package in favor of using the asChild prop for better flexibility.

To style the Next.js image component, use the asChild prop on the Box component.

<Box asChild>
  <NextImage />
</Box>

To style the Next.js link component, use the asChild prop on the Link component

<Link isExternal asChild>
  <NextLink />
</Link>

Theme Tools

We've removed this package in favor using CSS color mix.

Before

We used JS to resolve the colors and then apply the transparency

defineStyle({
  bg: transparentize("blue.200", 0.16)(theme),
  // -> rgba(0, 0, 255, 0.16)
})

After

We now use CSS color-mix

defineStyle({
  bg: "blue.200/16",
  // -> color-mix(in srgb, var(--chakra-colors-200), transparent 16%)
})

forwardRef

Due to the simplification of the as prop, we no longer provide a custom forwardRef. Prefer to use forwardRef from React directly.

Before:

import { Button as ChakraButton, forwardRef } from "@chakra-ui/react"

const Button = forwardRef<ButtonProps, "button">(function Button(props, ref) {
  return <ChakraButton ref={ref} {...props} />
})

After:

import { Button as ChakraButton } from "@chakra-ui/react"
import { forwardRef } from "react"

const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  function Button(props, ref) {
    return <ChakraButton ref={ref} {...props} />
  },
)

Icons

Removed @chakra-ui/icons package. Prefer to use lucide-react or react-icons instead.

Storybook Addon

We're removed the storybook addon in favor of using @storybook/addon-themes and withThemeByClassName helper.

import { ChakraProvider, defaultSystem } from "@chakra-ui/react"
import { withThemeByClassName } from "@storybook/addon-themes"
import type { Preview, ReactRenderer } from "@storybook/react"

const preview: Preview = {
  decorators: [
    withThemeByClassName<ReactRenderer>({
      defaultTheme: "light",
      themes: {
        light: "",
        dark: "dark",
      },
    }),
    (Story) => (
      <ChakraProvider value={defaultSystem}>
        <Story />
      </ChakraProvider>
    ),
  ],
}

export default preview

Removed Components

  • StackItem: You don't need this anymore. Use Box instead.
  • FocusLock: We no longer ship a focus lock component. Install and use react-focus-lock directly.
  • AlertDialog
    • Replace with the Dialog component and set role=alertdialog
    • Set leastDestructiveRef prop to the initialFocusEl to the Dialog.Root component

CircularProgress

  • Renamed to ProgressCircle and now uses compound components
  • isIndeterminate becomes value={null}
  • thickness prop becomes --thickness CSS variable
  • color prop becomes stroke prop on ProgressCircle.Range

Before:

<CircularProgress
  value={75}
  thickness="4px"
  color="blue.500"
  isIndeterminate={false}
/>

After:

<ProgressCircle.Root value={75}>
  <ProgressCircle.Circle css={{ "--thickness": "4px" }}>
    <ProgressCircle.Track />
    <ProgressCircle.Range stroke="blue.500" />
  </ProgressCircle.Circle>
</ProgressCircle.Root>

For indeterminate progress:

<ProgressCircle.Root value={null}>
  <ProgressCircle.Circle>
    <ProgressCircle.Track />
    <ProgressCircle.Range />
  </ProgressCircle.Circle>
</ProgressCircle.Root>

StackDivider

  • No longer available as a separate component
  • Use explicit Stack.Separator components between stack items

Before:

<VStack divider={<StackDivider borderColor="gray.200" />} spacing={4}>
  <Box>Item 1</Box>
  <Box>Item 2</Box>
  <Box>Item 3</Box>
</VStack>

After:

<VStack gap={4}>
  <Box>Item 1</Box>
  <Stack.Separator borderColor="gray.200" />
  <Box>Item 2</Box>
  <Stack.Separator borderColor="gray.200" />
  <Box>Item 3</Box>
</VStack>

Prop Changes

Boolean Props

Changed naming convention for boolean properties from is<X> to <x>

  • isOpen -> open
  • defaultIsOpen -> defaultOpen
  • isDisabled -> disabled
  • isInvalid -> invalid
  • isRequired -> required

ColorScheme Prop

The colorScheme prop has been changed to colorPalette

Before

  • You could only use colorScheme in a component's theme
  • colorScheme clashes with the native colorScheme prop in HTML elements
<Button colorScheme="blue">Click me</Button>

After

  • You can now use colorPalette anywhere
<Button colorPalette="blue">Click me</Button>

Usage in any component, you can do something like:

<Box colorPalette="red">
  <Box bg="colorPalette.400">Some box</Box>
  <Text color="colorPalette.600">Some text</Text>
</Box>

If you are using custom colors, you must define two things to make colorPalette work:

  • tokens: For the 50-950 color palette
  • semanticTokens: For the solid, contrast, fg, muted, subtle, emphasized, and focusRing color keys

theme.ts

import { createSystem, defaultConfig } from "@chakra-ui/react"

export const system = createSystem(defaultConfig, {
  theme: {
    tokens: {
      colors: {
        brand: {
          50: { value: "#e6f2ff" },
          100: { value: "#e6f2ff" },
          200: { value: "#bfdeff" },
          300: { value: "#99caff" },
          // ...
          950: { value: "#001a33" },
        },
      },
    },
    semanticTokens: {
      colors: {
        brand: {
          solid: { value: "{colors.brand.500}" },
          contrast: { value: "{colors.brand.100}" },
          fg: { value: "{colors.brand.700}" },
          muted: { value: "{colors.brand.100}" },
          subtle: { value: "{colors.brand.200}" },
          emphasized: { value: "{colors.brand.300}" },
          focusRing: { value: "{colors.brand.500}" },
        },
      },
    },
  },
})

Read more about it here.

Gradient Props

Gradient style prop simplified to gradient and gradientFrom and gradientTo props. This reduces the runtime performance cost of parsing the gradient string, and allows for better type inference.

Before

<Box bgGradient="linear(to-r, red.200, pink.500)" />

After

<Box bgGradient="to-r" gradientFrom="red.200" gradientTo="pink.500" />

Color Palette

  • Default color palette is now gray for all components but you can configure this in your theme.

  • Default theme color palette size has been increased to 11 shades to allow more color variations.

    Before

    const colors = {
      // ...
      gray: {
        50: "#F7FAFC",
        100: "#EDF2F7",
        200: "#E2E8F0",
        300: "#CBD5E0",
        400: "#A0AEC0",
        500: "#718096",
        600: "#4A5568",
        700: "#2D3748",
        800: "#1A202C",
        900: "#171923",
      },
    }

    After

    const colors = {
      // ...
      gray: {
        50: { value: "#fafafa" },
        100: { value: "#f4f4f5" },
        200: { value: "#e4e4e7" },
        300: { value: "#d4d4d8" },
        400: { value: "#a1a1aa" },
        500: { value: "#71717a" },
        600: { value: "#52525b" },
        700: { value: "#3f3f46" },
        800: { value: "#27272a" },
        900: { value: "#18181b" },
        950: { value: "#09090b" },
      },
    }

Style Props

Changed the naming convention for some style props

  • noOfLines -> lineClamp
  • truncated -> truncate
  • _activeLink -> _currentPage
  • _activeStep -> _currentStep
  • _mediaDark -> _osDark
  • _mediaLight -> _osLight

Examples:

// Before
<Text noOfLines={2}>
  Long text that will be clamped to 2 lines
</Text>

<Text truncated>
  This text will be truncated with ellipsis
</Text>

// After
<Text lineClamp={2}>
  Long text that will be clamped to 2 lines
</Text>

<Text truncate>
  This text will be truncated with ellipsis
</Text>

We removed the apply prop in favor of textStyle or layerStyles

Nested Styles

We have changed the way you write nested styles in Chakra UI components.

Before

Write nested styles using the sx or __css prop, and you sometimes don't get auto-completion for nested styles.

<Box
  sx={{
    svg: { color: "red.500" },
  }}
/>

After

Write nested styles using the css prop. All nested selectors require the use of the ampersand & prefix

<Box
  css={{
    "& svg": { color: "red.500" },
  }}
/>

This was done for two reasons:

  • Faster style processing: Before we had to check if a style key is a style prop or a selector which is quite expensive overall.
  • Better typings: This makes it easier to type nested style props are strongly typed

Component Changes

ChakraProvider

  • Removed theme prop in favor of passing the system prop instead. Import the defaultSystem module instead of theme

  • Removed resetCss prop in favor of passing preflight: false to the createSystem function

Before

<ChakraProvider resetCss={false}>
  <Component />
</ChakraProvider>

After

const system = createSystem(defaultConfig, { preflight: false })

<Provider value={system}>
  <Component />
</Provider>
  • Removed support for configuring toast options. Pass it to the createToaster function in components/ui/toaster.tsx file instead.
  • Renamed to Dialog
  • Remove isCentered prop in favor of using the placement=center prop
  • Removed isOpen and onClose props in favor of using the open and onOpenChange props

Avatar

  • Remove max prop in favor of userland control
  • Remove excess label part
  • Move image related props to Avatar.Image component
  • Move fallback icon to Avatar.Fallback component
  • Move name prop to Avatar.Fallback component

Portal

  • Remove appendToParentPortal prop in favor of using the containerRef
  • Remove PortalManager component

Progress

  • Now uses compound components with Progress.Root, Progress.Track, and Progress.Range
  • hasStripe prop renamed to striped
  • isAnimated prop renamed to animated
  • colorScheme prop renamed to colorPalette

Before:

<Progress hasStripe isAnimated value={75} colorScheme="blue" />

After:

<Progress.Root striped animated value={75} colorPalette="blue">
  <Progress.Track>
    <Progress.Range />
  </Progress.Track>
</Progress.Root>

Stack

  • Changed spacing to gap
  • Removed StackItem in favor of using the Box component directly

Select

Now called NativeSelect and exposes all parts now.

Before:

<Select placeholder="Select option">
  <option value="option1">Option 1</option>
  <option value="option2">Option 2</option>
  <option value="option3">Option 3</option>
</Select>

After:

<NativeSelect.Root size="sm" width="240px">
  <NativeSelect.Field placeholder="Select option">
    <option value="option1">Option 1</option>
    <option value="option2">Option 2</option>
    <option value="option3">Option 3</option>
  </NativeSelect.Field>
  <NativeSelect.Indicator />
</NativeSelect.Root>

Changing the icon

Before:

<Select icon={<MdArrowDropDown />} placeholder="Woohoo! A new icon" />

After:

<NativeSelect.Indicator>
  <MdArrowDropDown />
</NativeSelect.Indicator>

Collapse

  • Rename Collapse to Collapsible namespace
  • Rename in to open
  • animateOpacity has been removed, use keyframes animations expand-height and collapse-height instead

Before

<Collapse in={isOpen} animateOpacity>
  Some content
</Collapse>

After

<Collapsible.Root open={isOpen}>
  <Collapsible.Content>Some content</Collapsible.Content>
</Collapsible.Root>

Image

  • Now renders a native img without any fallback
  • Remove fallbackSrc due to the SSR issues it causes
  • Remove useImage hook
  • Remove Img in favor of using the Image component directly

PinInput

  • Changed value, defaultValue to use string[] instead of string
  • onChange prop is now called onValueChange
  • Add new PinInput.Control and PinInput.Label component parts
  • PinInput.Root now renders a div element by default. Consider combining with Stack or Group for better layout control
  • onComplete prop is now called onValueComplete

NumberInput

  • Rename NumberInputStepper to NumberInput.Control
  • Rename NumberInputStepperIncrement to NumberInput.IncrementTrigger
  • Rename NumberInputStepperDecrement to NumberInput.DecrementTrigger
  • onChange prop is now called onValueChange
  • Remove focusBorderColor and errorBorderColor, consider setting the --focus-color and --error-color css variables instead
  • onInvalid prop is now called onValueInvalid
  • parse and format props removed in favor of formatOptions prop

Before

<NumberInput>
  <NumberInputField />
  <NumberInputStepper>
    <NumberIncrementStepper />
    <NumberDecrementStepper />
  </NumberInputStepper>
</NumberInput>

After

<NumberInput.Root>
  <NumberInput.Input />
  <NumberInput.Control>
    <NumberInput.IncrementTrigger />
    <NumberInput.DecrementTrigger />
  </NumberInput.Control>
</NumberInput.Root>

Divider

  • Rename to Separator
  • Switch to div element for better layout control
  • Simplify component to rely on borderTopWidth and borderInlineStartWidth
  • To change the thickness reliably, set the --divider-border-width css variable

Input, Select, Textarea

  • Removed invalid prop in favor of wrapping the component in a Field component. This allows for adding a label, error text and asterisk easily.

Before

<Input invalid />

After

<Field.Root invalid>
  <Field.Label>Email</Field.Label>
  <Input />
  <Field.ErrorText>This field is required</Field.ErrorText>
</Field.Root>
  • Removed isExternal prop in favor of explicitly setting the target and rel props

Before

<Link isExternal>Click me</Link>

After

<Link target="_blank" rel="noopener noreferrer">
  Click me
</Link>

Button

  • Removed isActive in favor of passing data-active

Before

<Button isActive>Click me</Button>

After

<Button data-active>Click me</Button>

IconButton

  • Removed icon prop in favor of rendering the children prop directly
  • Removed isRounded in favor of using the borderRadius=full prop

Spinner

  • Change the thickness prop to borderWidth
  • Change the speed prop to animationDuration

Before

<Spinner thickness="2px" speed="0.5s" />

After

<Spinner borderWidth="2px" animationDuration="0.5s" />

Dialog, Drawer

  • isOpen and onChange props have been removed in favor of open and onOpenChange props
  • blockScrollOnMount is now preventScroll
  • closeOnEsc is now closeOnEscape
  • closeOnOverlayClick is now closeOnInteractOutside
  • initialFocusRef is now an initialFocusEl function that returns the element
  • finalFocusRef is now an finalFocusEl function that returns the element

Editable

  • finalFocusRef is now finalFocusEl function that returns the element
  • isDisabled is now disabled
  • onSubmit is now onValueCommit
  • onCancel is now onValueRevert
  • onChange is now onValueChange
  • startWithEditView is now defaultEdit
  • Replace submitOnBlur with submitMode

FormControl

  • Replace FormControl with the Field component.
  • Replace FormErrorMessage with the Field.ErrorText component.

Before:

<FormControl>
  <Input />
  <FormErrorMessage>This field is required</FormErrorMessage>
</FormControl>

After:

<Field.Root>
  <Input />
  <Field.ErrorText>This field is required</Field.ErrorText>
</Field.Root>

Collapse

Replace with the Collapsible component.

Before:

<Collapse in={isOpen} animateOpacity>
  Some content
</Collapse>

After:

<Collapsible.Root open={isOpen}>
  <Collapsible.Content>Some content</Collapsible.Content>
</Collapsible.Root>

Slider

  • onChange prop is now called onValueChange
  • onChangeEnd prop is now called onValueChangeEnd
  • onChangeStart prop is now removed
  • isReversed prop is now removed

RangeSlider

Can now be used as a single slider by passing an array of values

Before:

<RangeSlider defaultValue={[10, 30]}>
  <RangeSliderTrack>
    <RangeSliderFilledTrack />
  </RangeSliderTrack>
  <RangeSliderThumb index={0} />
  <RangeSliderThumb index={1} />
</RangeSlider>

After:

<Slider.Root defaultValue={[10, 30]}>
  <Slider.Control>
    <Slider.Track>
      <Slider.Range />
    </Slider.Track>
    <Slider.Thumbs />
  </Slider.Control>
</Slider.Root>

Table

  • TableContainer is now Table.ScrollArea
  • Td(now called Table.ColumnHeader) isNumeric is now textAlign="end"

The compound component have been renamed slightly.

Before:

<Table variant="simple">
  <TableCaption>Imperial to metric conversion factors</TableCaption>
  <Thead>
    <Tr>
      <Th>Product</Th>
      <Th>Category</Th>
      <Th isNumeric>Price</Th>
    </Tr>
  </Thead>
  <Tbody>
    {items.map((item) => (
      <Tr key={item.id}>
        <Td>{item.name}</Td>
        <Td>{item.category}</Td>
        <Td isNumeric>{item.price}</Td>
      </Tr>
    ))}
  </Tbody>
  <Tfoot>
    <Tr>
      <Th>Product</Th>
      <Th>Category</Th>
      <Th isNumeric>Price</Th>
    </Tr>
  </Tfoot>
</Table>

After:

<Table.Root size="sm">
  <Table.Header>
    <Table.Row>
      <Table.ColumnHeader>Product</Table.ColumnHeader>
      <Table.ColumnHeader>Category</Table.ColumnHeader>
      <Table.ColumnHeader textAlign="end">Price</Table.ColumnHeader>
    </Table.Row>
  </Table.Header>
  <Table.Body>
    {items.map((item) => (
      <Table.Row key={item.id}>
        <Table.Cell>{item.name}</Table.Cell>
        <Table.Cell>{item.category}</Table.Cell>
        <Table.Cell textAlign="end">{item.price}</Table.Cell>
      </Table.Row>
    ))}
  </Table.Body>
</Table.Root>

Tag

TagLeftIcon and TagRightIcon are now Tag.StartElement and Tag.EndElement

Before:

<Tag>
  <TagLeftIcon boxSize="12px" as={AddIcon} />
  <TagLabel>Cyan</TagLabel>
  <TagRightIcon boxSize="12px" as={AddIcon} />
</Tag>

After:

<Tag.Root>
  <Tag.StartElement>
    <AddIcon />
  </Tag.StartElement>
  <Tag.Label>Cyan</Tag.Label>
  <Tag.EndElement>
    <AddIcon />
  </Tag.EndElement>
</Tag.Root>
  • TagCloseButton is now Tag.CloseTrigger

Before:

<Tag>
  <TagLabel>Green</TagLabel>
  <TagCloseButton />
</Tag>

After:

<Tag.Root>
  <Tag.Label>Green</Tag.Label>
  <Tag.CloseTrigger />
</Tag.Root>

Alert

  • AlertIcon is now Alert.Indicator

Before:

<Alert>
  <AlertIcon />
  <AlertTitle>Your browser is outdated!</AlertTitle>
  <AlertDescription>Your Chakra experience may be degraded.</AlertDescription>
</Alert>

After:

<Alert.Root status="error">
  <Alert.Indicator />
  <Alert.Content>
    <Alert.Title>Invalid Fields</Alert.Title>
    <Alert.Description>
      Your form has some errors. Please fix them and try again.
    </Alert.Description>
  </Alert.Content>
</Alert.Root>
  • Removed addRoleprop in favor of role prop.

Skeleton

  • startColor and endColor props now use CSS variables

Before:

<Skeleton startColor="pink.500" endColor="orange.500" />

After:

<Skeleton
  css={{
    "--start-color": "colors.pink.500",
    "--end-color": "colors.orange.500",
  }}
/>
  • isLoaded prop is now loading

Before:

<Skeleton isLoaded>
  <span>Chakra ui is cool</span>
</Skeleton>

After:

<Skeleton loading={false}>
  <span>Chakra ui is cool</span>
</Skeleton>
  • Now uses compound components everywhere

Before:

<Menu>
  <MenuButton as={Button} rightIcon={<ChevronDownIcon />}>
    Actions
  </MenuButton>
  <MenuList>
    <MenuItem>Download</MenuItem>
    <MenuItem>Create a Copy</MenuItem>
  </MenuList>
</Menu>

After:

<Menu.Root>
  <Menu.Trigger asChild>
    <Button>
      Actions
      <ChevronDownIcon />
    </Button>
  </Menu.Trigger>
  <Portal>
    <Menu.Positioner>
      <Menu.Content>
        <Menu.Item value="download">Download</Menu.Item>
        <Menu.Item value="copy">Create a Copy</Menu.Item>
      </Menu.Content>
    </Menu.Positioner>
  </Portal>
</Menu.Root>
  • Accesing internal state is now done via Menu.Context no longer render prop.

Before:

<Menu>
  {({ isOpen }) => (
    <>
      <MenuButton isActive={isOpen} as={Button} rightIcon={<ChevronDownIcon />}>
        {isOpen ? "Close" : "Open"}
      </MenuButton>
      <MenuList>
        <MenuItem>Download</MenuItem>
        <MenuItem onClick={() => alert("Kagebunshin")}>Create a Copy</MenuItem>
      </MenuList>
    </>
  )}
</Menu>

After:

<Menu.Root>
  <Menu.Context>
    {(menu) => (
      <Menu.Trigger asChild>
        <Button>
          {menu.open ? "Close" : "Open"}
          <ChevronDownIcon />
        </Button>
      </Menu.Trigger>
    )}
  </Menu.Context>
  <Portal>
    <Menu.Positioner>
      <Menu.Content>
        <Menu.Item value="download">Download</Menu.Item>
        <Menu.Item value="copy" onSelect={() => alert("Kagebunshin")}>
          Create a Copy
        </Menu.Item>
      </Menu.Content>
    </Menu.Positioner>
  </Portal>
</Menu.Root>
  • isLazy prop on Menu is split into lazyMount and unmountOnExit on Menu.Root

  • MenuOptionGroup is now split into Menu.RadioItemGroup and Menu.CheckboxItemGroup to handle the states separately.

Before:

<Menu>
  <MenuButton as={Button}>Trigger</MenuButton>
  <MenuList>
    <MenuOptionGroup defaultValue="asc" title="Order" type="radio">
      <MenuItemOption value="asc">Ascending</MenuItemOption>
      <MenuItemOption value="desc">Descending</MenuItemOption>
    </MenuOptionGroup>
    <MenuDivider />
    <MenuOptionGroup title="Country" type="checkbox">
      <MenuItemOption value="email">Email</MenuItemOption>
      <MenuItemOption value="phone">Phone</MenuItemOption>
      <MenuItemOption value="country">Country</MenuItemOption>
    </MenuOptionGroup>
  </MenuList>
</Menu>

After:

<Menu.Root>
  <Menu.Trigger asChild>
    <Button>Trigger</Button>
  </Menu.Trigger>
  <Portal>
    <Menu.Positioner>
      <Menu.Content minW="10rem">
        <Menu.RadioItemGroup defaultValue="asc">
          <Menu.RadioItem value="asc">Ascending</Menu.RadioItem>
          <Menu.RadioItem value="desc">Descending</Menu.RadioItem>
        </Menu.RadioItemGroup>
        <Menu.CheckboxItemGroup defaultValue={["email"]}>
          <Menu.CheckboxItem value="email">Email</Menu.CheckboxItem>
          <Menu.CheckboxItem value="phone">Phone</Menu.CheckboxItem>
          <Menu.CheckboxItem value="country">Country</Menu.CheckboxItem>
        </Menu.CheckboxItemGroup>
      </Menu.Content>
    </Menu.Positioner>
  </Portal>
</Menu.Root>

Tooltip

  • closeOnEsc now renamed to closeOnEscape

  • closeOnMouseDown is now closeOnPointerDown

  • placement, gutter, offset and arrow on Tooltip is now included as positioning prop on Tooltip.Root

Before:

<Tooltip placement="top" />

After:

<Tooltip.Root positioning={{ placement: "top" }} />

Accordion

  • These props have been changed:
    • allowMultiple -> multiple
    • allowToggle -> collapsible
    • index -> value
    • defaultIndex -> defaultValue

Before:

<Accordion allowMultiple index={[0]} onChange={() => {}} />

After:

<Accordion multiple value={["0"]} onValueChange={() => {}} />
  • AccordionButton is now Accordion.Trigger
  • AccordionIcon is now Accordion.ItemIndicator

Before:

<AccordionButton>Section 1 title</AccordionButton>

After:

<Accordion.Trigger>Section 1 title</Accordion.Trigger>

Tabs

  • Component structure has changed and value prop is now required on list and panels.

Before:

<Tabs>
  <TabList>
    <Tab>One</Tab>
    <Tab>Two</Tab>
    <Tab>Three</Tab>
  </TabList>
  <TabPanels>
    <TabPanel>one!</TabPanel>
    <TabPanel>two!</TabPanel>
    <TabPanel>three!</TabPanel>
  </TabPanels>
</Tabs>

After:

<Tabs.Root>
  <Tabs.List>
    <Tabs.Trigger value="one">One</Tabs.Trigger>
    <Tabs.Trigger value="two">Two</Tabs.Trigger>
    <Tabs.Trigger value="three">Three</Tabs.Trigger>
  </Tabs.List>
  <Tabs.Content value="one">one!</Tabs.Content>
  <Tabs.Content value="two">two!</Tabs.Content>
  <Tabs.Content value="three">three!</Tabs.Content>
</Tabs.Root>
  • defaultIndex, index and onChange is now defaultValue, value and onValueChange respectively

Before:

<Tabs defaultIndex={0} index={0} onChange={(index) => {}} />

After:

<Tabs defaultValue={0} value={0} onValueChange={({ value }) => {}} />
  • isLazy prop on Tabs is now lazyMount and unmountOnExit on Tabs.Root

Before:

<Tabs isLazy />

After:

<Tabs.Root lazyMount unmountOnExit />

Show and Hide

  • Show and Hide components are removed in favor of hideFrom and hideBelow

Before:

<Show below="md">
  This text appears only on screens md and smaller.
</Show>

<Hide below="md">
  This text hides at the "md" value screen width and smaller.
</Hide>

After:

<Box hideBelow="md">
  This text hides at the "md" value screen width and smaller.
</Box>

<Box hideFrom="md">
  This text appears only on screens md and larger.
</Box>

Checkbox

  • Refactored to use compound components

Before:

<Checkbox defaultChecked>Checkbox</Checkbox>

After:

<Checkbox.Root defaultChecked>
  <Checkbox.HiddenInput />
  <Checkbox.Control>
    <Checkbox.Indicator />
  </Checkbox.Control>
  <Checkbox.Label>Checkbox</Checkbox.Label>
</Checkbox.Root>

Radio Group

  • Refactored to use compound components

Before:

<RadioGroup defaultValue="2">
  <Radio value="1">Radio</Radio>
  <Radio value="2">Radio</Radio>
</RadioGroup>

After:

<RadioGroup.Root defaultValue="2">
  <RadioGroup.Item value="1">
    <RadioGroup.ItemHiddenInput />
    <RadioGroup.ItemIndicator />
    <RadioGroup.ItemText />
  </RadioGroup.Item>
</RadioGroup.Root>

Button Props

  • isActive → data-active attribute
  • isDisabled → disabled
  • isLoading → loading
  • leftIcon and rightIcon → passed as children
  • iconSpacing → removed (use gap in flex layout)
  • colorScheme → colorPalette

Example:

// Before
<Button
  isActive={true}
  isDisabled={false}
  isLoading={true}
  leftIcon={<Icon />}
  rightIcon={<Icon />}
  colorScheme="blue"
>
  Submit
</Button>

// After
<Button
  data-active=""
  disabled={false}
  loading={true}
  colorPalette="blue"
>
  <LeftIcon />
  Submit
  <RightIcon />
</Button>

Input Props

  • isDisabled → disabled
  • isInvalid → invalid
  • isReadOnly → readOnly
  • isRequired → required
  • colorScheme → colorPalette
  • focusBorderColor → use CSS variables
  • errorBorderColor → use CSS variables

Example:

// Before
<Input
  isDisabled={false}
  isInvalid={true}
  isReadOnly={false}
  isRequired={true}
  colorScheme="blue"
  focusBorderColor="blue.500"
  errorBorderColor="red.500"
/>

// After
<Input
  disabled={false}
  invalid={true}
  readOnly={false}
  required={true}
  colorPalette="blue"
  style={{
    "--focus-color": "blue.500",
    "--error-color": "red.500"
  }}
/>

Checkbox Props

  • isChecked → checked
  • isDisabled → disabled
  • isInvalid → invalid
  • isIndeterminate → indeterminate (on Indicator)
  • colorScheme → colorPalette
  • iconColor → removed (use CSS)
  • iconSize → removed (use CSS)
  • spacing → removed (use gap)

Example:

// Before
<Checkbox
  isChecked={true}
  isDisabled={false}
  isInvalid={true}
  isIndeterminate={true}
  colorScheme="blue"
>
  Accept terms
</Checkbox>

// After
<Checkbox.Root
  checked={true}
  disabled={false}
  invalid={true}
  colorPalette="blue"
>
  <Checkbox.Control>
    <Checkbox.Indicator indeterminate={true} />
  </Checkbox.Control>
  <Checkbox.Label>Accept terms</Checkbox.Label>
</Checkbox.Root>
  • isOpen → open
  • onClose → onOpenChange (different signature)
  • isCentered → placement="center"
  • scrollBehavior → same
  • motionPreset → updated values (e.g., slideInBottom → slide-in-bottom)
  • closeOnOverlayClick → closeOnInteractOutside
  • closeOnEsc → closeOnEscape
  • blockScrollOnMount → preventScroll
  • returnFocusOnClose → restoreFocus
  • initialFocusRef → initialFocusEl (function)
  • finalFocusRef → finalFocusEl (function)

Example:

// Before
<Modal
  isOpen={isOpen}
  onClose={onClose}
  isCentered={true}
  closeOnOverlayClick={true}
  initialFocusRef={initialRef}
>
  <ModalOverlay />
  <ModalContent>
    <ModalHeader>Title</ModalHeader>
    <ModalBody>Content</ModalBody>
  </ModalContent>
</Modal>

// After
<Dialog.Root
  open={isOpen}
  onOpenChange={(e) => !e.open && onClose()}
  placement="center"
  closeOnInteractOutside={true}
  initialFocusEl={() => initialRef.current}
>
  <Dialog.Backdrop />
  <Dialog.Positioner>
    <Dialog.Content>
      <Dialog.Header>
        <Dialog.Title>Title</Dialog.Title>
      </Dialog.Header>
      <Dialog.Body>Content</Dialog.Body>
    </Dialog.Content>
  </Dialog.Positioner>
</Dialog.Root>

Stack Props

  • spacing → gap
  • divider → separator
  • Other props remain the same

Example:

// Before
<Stack
  spacing="4"
  divider={<StackDivider />}
>
  <Box>Item 1</Box>
  <Box>Item 2</Box>
</Stack>

// After
<Stack
  gap="4"
  separator={<Stack.Separator />}
>
  <Box>Item 1</Box>
  <Box>Item 2</Box>
</Stack>

Previous

Installation

Next

CLI