apps/www/content/docs/get-started/migration.mdx
:::warning
We recommend using the LLMs.txt files to make the Chakra UI v3 documentation available to large language models.
:::
The codemod automates the migration from Chakra UI v2 to v3. It handles component renames, prop changes, import updates, and compound component restructuring. Start here before migrating manually.
npx @chakra-ui/codemod upgrade
Use --dry to preview changes without modifying files.
The minimum node version required is Node.20.x
:::steps
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
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.
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
defaultSystemwith the customsystem
The Provider component compose the ChakraProvider from Chakra and
ThemeProvider from next-themes
:::
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.
ColorModeProvider and useColorMode have been removed in favor of
next-themesLightMode, 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
:::
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.
We removed the styleConfig and multiStyleConfig concept in favor of recipes
and slot recipes. This pattern was inspired by Panda CSS.
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>
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%)
})
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} />
},
)
Removed @chakra-ui/icons package. Use react-icons (Lucide icons recommended)
or lucide-react instead. Install with npm install react-icons.
Icon from @chakra-ui/reactBefore:
import { AddIcon, CheckIcon } from "@chakra-ui/icons"
<AddIcon />
<CheckIcon boxSize={6} color="green.500" />
After:
import { Icon } from "@chakra-ui/react"
import { LuCheck, LuPlus } from "react-icons/lu"
<LuPlus />
<Icon as={LuCheck} boxSize={6} color="green.500" />
Common icon mappings: AddIcon → LuPlus, CloseIcon → LuX, CheckIcon →
LuCheck, EditIcon → LuPencil, DeleteIcon → LuTrash2, SearchIcon →
LuSearch, ChevronDownIcon → LuChevronDown, ArrowForwardIcon →
LuArrowRight, HamburgerIcon → LuMenu, WarningIcon → LuAlertTriangle,
InfoIcon → LuInfo, ExternalLinkIcon → LuExternalLink, StarIcon →
LuStar
In v2, <Icon> rendered an <svg> wrapper and you passed SVG children (like
<path>) directly. In v3, when using custom SVGs, use the asChild prop so the
Icon merges its styles onto your <svg> element.
Before:
import { Icon } from "@chakra-ui/react"
;<Icon viewBox="0 0 24 24" color="red.500" boxSize={6}>
<path d="M12 2L2 22h20L12 2z" fill="currentColor" />
</Icon>
After:
import { Icon } from "@chakra-ui/react"
;<Icon color="red.500" size="md" asChild>
<svg viewBox="0 0 24 24">
<path d="M12 2L2 22h20L12 2z" fill="currentColor" />
</svg>
</Icon>
Alternatively, use createIcon to define reusable custom icons:
import { createIcon } from "@chakra-ui/react"
const TriangleIcon = createIcon({
displayName: "TriangleIcon",
viewBox: "0 0 24 24",
path: <path d="M12 2L2 22h20L12 2z" fill="currentColor" />,
})
// Usage
<TriangleIcon size="lg" color="red.500" />
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
Box instead.react-focus-lock directly.Dialog component and set role=alertdialogleastDestructiveRef prop to the initialFocusEl to the Dialog.Root
componentProgressCircle and now uses compound componentsisIndeterminate becomes value={null}thickness prop becomes --thickness CSS variablecolor prop becomes stroke prop on ProgressCircle.RangeBefore:
<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>
Stack.Separator components between stack itemsBefore:
<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>
Changed naming convention for boolean properties from is<X> to <x>
isOpen -> opendefaultIsOpen -> defaultOpenisDisabled -> disabledisInvalid -> invalidisRequired -> requiredThe colorScheme prop has been changed to colorPalette
Before
colorScheme in a component's themecolorScheme clashes with the native colorScheme prop in HTML elements<Button colorScheme="blue">Click me</Button>
After
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:
solid, contrast, fg, muted, subtle,
emphasized, and focusRing color keysimport { 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 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" />
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" },
},
}
Changed the naming convention for some style props
noOfLines -> lineClamptruncated -> truncate_activeLink -> _currentPage_activeStep -> _currentStep_mediaDark -> _osDark_mediaLight -> _osLightExamples:
// 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
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:
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>
createToaster
function in components/ui/toaster.tsx file instead.Renamed to Dialog and uses compound components with an explicit
Dialog.Positioner and Portal wrapper.
Component Renaming:
Modal → Dialog.RootModalOverlay → Dialog.BackdropModalContent → Dialog.Content (wrap in Dialog.Positioner)ModalHeader → Dialog.HeaderModalBody → Dialog.BodyModalFooter → Dialog.FooterModalCloseButton → Dialog.CloseTriggerProp Changes:
isOpen → openonClose → onOpenChange (receives { open })isCentered → placement="center"closeOnOverlayClick → closeOnInteractOutsidecloseOnEsc → closeOnEscapeblockScrollOnMount → preventScrollonOverlayClick → onInteractOutsideonEsc → onEscapeKeyDownonCloseComplete → onExitCompleteinitialFocusRef → initialFocusEl={() => ref.current}finalFocusRef → finalFocusEl={() => ref.current}Size Mapping: Sizes 2xl through 6xl are mapped to xl in v3.
Removed Props: allowPinchZoom, lockFocusAcrossFrames,
preserveScrollBarGap, returnFocusOnClose, useInert, portalProps
Now uses a declarative composition pattern with separate Avatar.Image and
Avatar.Fallback parts.
Component Renaming:
Avatar → Avatar.RootAvatarBadge → removed (use Float + Circle instead)AvatarGroup → AvatarGroup (unchanged, but max prop removed)Props moved to Avatar.Image:
src, srcSet, sizes, loading, referrerPolicy, crossOriginProps moved to Avatar.Fallback:
name — generates initials automaticallyicon — render as children insteadiconLabel → aria-labelProps removed:
ignoreFallback — no longer neededshowBorder — use border and borderColor style props insteadAvatarGroup max — removed, handle in userlandAvatarGroup spacing → spaceXBefore:
import { Avatar, AvatarBadge, AvatarGroup } from "@chakra-ui/react"
const Demo = () => (
<>
<Avatar name="Dan Abrahmov" src="https://bit.ly/dan-abramov" size="md" />
<Avatar bg="red.500" icon={<AiOutlineUser />} />
<Avatar>
<AvatarBadge boxSize="1.25em" bg="green.500" />
</Avatar>
</>
)
After:
import { Avatar, AvatarGroup, Circle, Float } from "@chakra-ui/react"
const Demo = () => (
<>
<Avatar.Root size="md">
<Avatar.Fallback name="Dan Abrahmov" />
<Avatar.Image src="https://bit.ly/dan-abramov" />
</Avatar.Root>
<Avatar.Root bg="red.500">
<Avatar.Fallback>
<AiOutlineUser />
</Avatar.Fallback>
</Avatar.Root>
<Avatar.Root>
<Avatar.Image src="https://bit.ly/dan-abramov" />
<Float placement="bottom-end" offsetX="1" offsetY="1">
<Circle
bg="green.500"
size="8px"
outline="0.2em solid"
outlineColor="bg"
/>
</Float>
</Avatar.Root>
</>
)
Now uses compound components with explicit separators between items and a
required Breadcrumb.List wrapper.
Component Renaming:
Breadcrumb → Breadcrumb.RootBreadcrumbItem → Breadcrumb.ItemBreadcrumbLink → Breadcrumb.LinkBreadcrumbLink with isCurrentPage → Breadcrumb.CurrentLinkBreadcrumbSeparator → Breadcrumb.SeparatorProp Changes:
separator prop → removed, use explicit <Breadcrumb.Separator /> between
itemsspacing → gap (moved to Breadcrumb.List)isCurrentPage on BreadcrumbItem → use Breadcrumb.CurrentLink insteadisLastChild → removed (not needed with explicit separators)listProps → spread directly on Breadcrumb.ListBefore:
import { Breadcrumb, BreadcrumbItem, BreadcrumbLink } from "@chakra-ui/react"
const Demo = () => (
<Breadcrumb separator="-" spacing="8px">
<BreadcrumbItem>
<BreadcrumbLink href="#">Home</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbItem isCurrentPage>
<BreadcrumbLink href="#">Current</BreadcrumbLink>
</BreadcrumbItem>
</Breadcrumb>
)
After:
import { Breadcrumb } from "@chakra-ui/react"
const Demo = () => (
<Breadcrumb.Root>
<Breadcrumb.List gap="8px">
<Breadcrumb.Item>
<Breadcrumb.Link href="#">Home</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator>-</Breadcrumb.Separator>
<Breadcrumb.Item>
<Breadcrumb.CurrentLink>Current</Breadcrumb.CurrentLink>
</Breadcrumb.Item>
</Breadcrumb.List>
</Breadcrumb.Root>
)
appendToParentPortal prop in favor of using the containerRefPortalManager componentProgress.Root, Progress.Track, and
Progress.RangehasStripe prop renamed to stripedisAnimated prop renamed to animatedcolorScheme prop renamed to colorPaletteBefore:
<Progress hasStripe isAnimated value={75} colorScheme="blue" />
After:
<Progress.Root striped animated value={75} colorPalette="blue">
<Progress.Track>
<Progress.Range />
</Progress.Track>
</Progress.Root>
spacing to gapStackItem in favor of using the Box component directlyNow 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 to Collapsible namespacein to openanimateOpacity has been removed, use keyframes animations expand-height
and collapse-height insteadBefore
<Collapse in={isOpen} animateOpacity>
Some content
</Collapse>
After
<Collapsible.Root open={isOpen}>
<Collapsible.Content>Some content</Collapsible.Content>
</Collapsible.Root>
Now renders a native img without built-in fallback logic. Img has been
consolidated into Image.
Img → Imagefit → objectFitalign → objectPositionfallbackSrc, fallback, ignoreFallback, fallbackStrategy → removeduseImage hook → removedBefore:
import { Img } from "@chakra-ui/react"
const Demo = () => (
)
After:
import { Image } from "@chakra-ui/react"
const Demo = () => (
<Image src="photo.jpg" objectFit="cover" objectPosition="center" />
)
For fallback behavior, use the native
onErrorevent to swap thesrc.
Now uses compound components. Each input requires an index prop and must be
wrapped in PinInput.Control.
Component Renaming:
PinInput → PinInput.RootPinInputField → PinInput.Input (requires index prop)Prop Changes:
value / defaultValue → now string[] instead of stringonChange → onValueChange (receives { value, valueAsString })onComplete → onValueComplete (receives { value, valueAsString })isDisabled → disabledisInvalid → invalidmanageFocus → removedBefore:
import { PinInput, PinInputField } from "@chakra-ui/react"
const Demo = () => (
<PinInput defaultValue="23" onChange={setValue} onComplete={handleComplete}>
<PinInputField />
<PinInputField />
<PinInputField />
</PinInput>
)
After:
import { PinInput } from "@chakra-ui/react"
const Demo = () => (
<PinInput.Root
defaultValue={["2", "3"]}
onValueChange={(e) => setValue(e.value)}
onValueComplete={(e) => handleComplete(e.value)}
>
<PinInput.HiddenInput />
<PinInput.Control>
<PinInput.Input index={0} />
<PinInput.Input index={1} />
<PinInput.Input index={2} />
</PinInput.Control>
</PinInput.Root>
)
Now uses compound components with an explicit Popover.Positioner wrapper
around content. PopoverTrigger now requires asChild.
Component Renaming:
Popover → Popover.RootPopoverTrigger → Popover.Trigger (add asChild)PopoverContent → Popover.Content (wrap in Popover.Positioner)PopoverHeader → Popover.TitlePopoverBody → Popover.BodyPopoverFooter → Popover.FooterPopoverArrow → Popover.ArrowPopoverCloseButton → Popover.CloseTriggerPopoverAnchor → Popover.AnchorProp Changes:
isOpen → opendefaultIsOpen → defaultOpenonClose / onOpen → onOpenChange (receives { open })closeOnBlur → closeOnInteractOutsidecloseOnEsc → closeOnEscapeisLazy → lazyMountlazyBehavior="unmount" → unmountOnExitinitialFocusRef → initialFocusEl={() => ref.current}trigger="hover" → use HoverCard component insteadplacement, gutter, flip, offset, matchWidth,
strategy) → grouped into positioning objectmatchWidth → positioning.sameWidthRemoved Props: computePositionOnMount, returnFocusOnClose,
arrowShadowColor, modifiers
Before:
import {
Popover,
PopoverArrow,
PopoverBody,
PopoverCloseButton,
PopoverContent,
PopoverHeader,
PopoverTrigger,
} from "@chakra-ui/react"
const Demo = () => (
<Popover placement="bottom" closeOnBlur={false} isLazy>
<PopoverTrigger>
<Button>Trigger</Button>
</PopoverTrigger>
<PopoverContent>
<PopoverArrow />
<PopoverCloseButton />
<PopoverHeader>Title</PopoverHeader>
<PopoverBody>Content here</PopoverBody>
</PopoverContent>
</Popover>
)
After:
import { Popover } from "@chakra-ui/react"
const Demo = () => (
<Popover.Root
positioning={{ placement: "bottom" }}
closeOnInteractOutside={false}
lazyMount
>
<Popover.Trigger asChild>
<Button>Trigger</Button>
</Popover.Trigger>
<Popover.Positioner>
<Popover.Content>
<Popover.Arrow />
<Popover.CloseTrigger />
<Popover.Title>Title</Popover.Title>
<Popover.Body>Content here</Popover.Body>
</Popover.Content>
</Popover.Positioner>
</Popover.Root>
)
Hover Trigger → HoverCard:
If you used trigger="hover", migrate to the HoverCard component:
Before:
<Popover trigger="hover" openDelay={500}>
<PopoverTrigger>
<Button>Hover me</Button>
</PopoverTrigger>
<PopoverContent>
<PopoverBody>Tooltip-like content</PopoverBody>
</PopoverContent>
</Popover>
After:
import { HoverCard } from "@chakra-ui/react"
const Demo = () => (
<HoverCard.Root openDelay={500}>
<HoverCard.Trigger asChild>
<Button>Hover me</Button>
</HoverCard.Trigger>
<HoverCard.Positioner>
<HoverCard.Content>
<HoverCard.Arrow />
Content here
</HoverCard.Content>
</HoverCard.Positioner>
</HoverCard.Root>
)
Component Renaming:
NumberInput → NumberInput.RootNumberInputField → NumberInput.InputNumberInputStepper → NumberInput.ControlNumberIncrementStepper → NumberInput.IncrementTriggerNumberDecrementStepper → NumberInput.DecrementTriggerProp Changes:
isDisabled → disabledisInvalid → invalidisReadOnly → readOnlyisRequired → requiredonChange → onValueChange (receives { value, valueAsNumber })onInvalid → onValueInvalidkeepWithinRange → allowOverflow (inverted: false → true)focusBorderColor / errorBorderColor → use --focus-color /
--error-color CSS variablesparse and format → removed, use formatOptions insteadBefore:
import {
NumberDecrementStepper,
NumberIncrementStepper,
NumberInput,
NumberInputField,
NumberInputStepper,
} from "@chakra-ui/react"
const Demo = () => (
<NumberInput
isDisabled
onChange={(valStr, valNum) => {}}
keepWithinRange={false}
>
<NumberInputField />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
)
After:
import { NumberInput } from "@chakra-ui/react"
const Demo = () => (
<NumberInput.Root disabled onValueChange={(e) => {}} allowOverflow>
<NumberInput.Input />
<NumberInput.Control>
<NumberInput.IncrementTrigger />
<NumberInput.DecrementTrigger />
</NumberInput.Control>
</NumberInput.Root>
)
Renamed to Separator to better align with semantic HTML and ARIA standards.
The component now uses a div element for better layout control.
Divider → SeparatorborderTopWidth and borderInlineStartWidth for styling--divider-border-width CSS variableorientation, variant, styling) remain the sameBefore:
import { Divider } from "@chakra-ui/react"
const Demo = () => (
<>
<Divider orientation="horizontal" />
<Divider orientation="vertical" height="20px" />
</>
)
After:
import { Separator } from "@chakra-ui/react"
const Demo = () => (
<>
<Separator orientation="horizontal" />
<Separator orientation="vertical" height="20px" />
</>
)
Now uses compound components with dot notation. The migration is straightforward — only component names change, all props remain the same.
Component Renaming:
Card → Card.RootCardHeader → Card.HeaderCardBody → Card.BodyCardFooter → Card.Footerv3 also introduces Card.Title and Card.Description as new semantic
components for better structure.
Before:
import {
Button,
Card,
CardBody,
CardFooter,
CardHeader,
Heading,
Text,
} from "@chakra-ui/react"
const Demo = () => (
<Card maxW="sm">
<CardHeader>
<Heading size="md">Living room Sofa</Heading>
</CardHeader>
<CardBody>
<Text>This sofa is perfect for modern tropical spaces.</Text>
<Text color="blue.600" fontSize="2xl">
$450
</Text>
</CardBody>
<CardFooter>
<Button variant="solid" colorScheme="blue">
Buy now
</Button>
</CardFooter>
</Card>
)
After:
import { Button, Card, Heading, Text } from "@chakra-ui/react"
const Demo = () => (
<Card.Root maxW="sm">
<Card.Header>
<Heading size="md">Living room Sofa</Heading>
</Card.Header>
<Card.Body>
<Text>This sofa is perfect for modern tropical spaces.</Text>
<Text color="blue.600" fontSize="2xl">
$450
</Text>
</Card.Body>
<Card.Footer>
<Button variant="solid" colorPalette="blue">
Buy now
</Button>
</Card.Footer>
</Card.Root>
)
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>
isExternal prop in favor of explicitly setting the target and
rel propsBefore
<Link isExternal>Click me</Link>
After
<Link target="_blank" rel="noopener noreferrer">
Click me
</Link>
Now uses compound components with dot notation. OrderedList and
UnorderedList are no longer separate components — use List.Root with the
as prop instead.
Component Renaming:
List → List.RootOrderedList → List.Root as="ol"UnorderedList → List.Root as="ul"ListItem → List.ItemListIcon → List.IndicatorProp Changes:
spacing → gapstyleType → listStyleTypestylePosition → listStylePositionBefore:
import { ListIcon, ListItem, UnorderedList } from "@chakra-ui/react"
import { MdCheckCircle } from "react-icons/md"
const Demo = () => (
<UnorderedList spacing={3}>
<ListItem>
<ListIcon as={MdCheckCircle} color="green.500" />
Lorem ipsum dolor sit amet
</ListItem>
<ListItem>
<ListIcon as={MdCheckCircle} color="green.500" />
Consectetur adipiscing elit
</ListItem>
</UnorderedList>
)
After:
import { List } from "@chakra-ui/react"
import { MdCheckCircle } from "react-icons/md"
const Demo = () => (
<List.Root as="ul" gap={3}>
<List.Item>
<List.Indicator as={MdCheckCircle} color="green.500" />
Lorem ipsum dolor sit amet
</List.Item>
<List.Item>
<List.Indicator as={MdCheckCircle} color="green.500" />
Consectetur adipiscing elit
</List.Item>
</List.Root>
)
For ordered lists:
Before:
import { ListItem, OrderedList } from "@chakra-ui/react"
const Demo = () => (
<OrderedList styleType="lower-roman" stylePosition="inside">
<ListItem>First item</ListItem>
<ListItem>Second item</ListItem>
</OrderedList>
)
After:
import { List } from "@chakra-ui/react"
const Demo = () => (
<List.Root as="ol" listStyleType="lower-roman" listStylePosition="inside">
<List.Item>First item</List.Item>
<List.Item>Second item</List.Item>
</List.Root>
)
Prop Changes:
isActive → data-active attributeisDisabled → disabledisLoading → loadingcolorScheme → colorPaletteleftIcon / rightIcon → render icons as children directlyiconSpacing → gapvariant="unstyled" → unstyled boolean propvariant="link" → variant="plain"Before:
<Button colorScheme="blue" isLoading leftIcon={<Download />} iconSpacing={2}>
Download
</Button>
After:
<Button colorPalette="blue" loading gap={2}>
<Download />
Download
</Button>
ButtonGroup Changes:
isAttached → attachedisDisabled → removed (propagate disabled to each child instead)icon → render as children directlyisRounded → borderRadius="full"isDisabled → disabledBefore:
<IconButton icon={<SearchIcon />} isRounded isDisabled aria-label="Search" />
After:
<IconButton borderRadius="full" disabled aria-label="Search">
<SearchIcon />
</IconButton>
thickness prop to borderWidthspeed prop to animationDurationBefore
<Spinner thickness="2px" speed="0.5s" />
After
<Spinner borderWidth="2px" animationDuration="0.5s" />
Both Modal and Drawer now use compound components with an explicit
Positioner and Portal wrapper.
Prop Changes (shared by Dialog and Drawer):
isOpen → openonClose → onOpenChange (receives { open })blockScrollOnMount → preventScrollcloseOnEsc → closeOnEscapecloseOnOverlayClick → closeOnInteractOutsideonOverlayClick → onInteractOutsideonEsc → onEscapeKeyDownonCloseComplete → onExitCompleteinitialFocusRef → initialFocusEl={() => ref.current}finalFocusRef → finalFocusEl={() => ref.current}isCentered → placement="center" (Dialog only)2xl–6xl → mapped to xlDrawer-specific Changes:
placement="left" → placement="start" (RTL-aware)placement="right" → placement="end" (RTL-aware)isFullHeight → add height="100%" to Drawer.ContentDrawerOverlay → Drawer.BackdropDrawerContent → Drawer.Positioner + Drawer.ContentRemoved Props: allowPinchZoom, lockFocusAcrossFrames,
preserveScrollBarGap, returnFocusOnClose, useInert, portalProps
Dialog Example:
Before:
import {
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
} from "@chakra-ui/react"
const Demo = () => (
<Modal isOpen={isOpen} onClose={onClose} isCentered closeOnEsc={false}>
<ModalOverlay />
<ModalContent>
<ModalCloseButton />
<ModalHeader>Title</ModalHeader>
<ModalBody>Content</ModalBody>
<ModalFooter>
<Button onClick={onClose}>Close</Button>
</ModalFooter>
</ModalContent>
</Modal>
)
After:
import { Dialog, Portal } from "@chakra-ui/react"
const Demo = () => (
<Dialog.Root
open={isOpen}
onOpenChange={(e) => !e.open && onClose()}
placement="center"
closeOnEscape={false}
>
<Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.CloseTrigger />
<Dialog.Header>Title</Dialog.Header>
<Dialog.Body>Content</Dialog.Body>
<Dialog.Footer>
<Button onClick={onClose}>Close</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.Root>
)
Drawer Example:
Before:
import {
Drawer,
DrawerBody,
DrawerCloseButton,
DrawerContent,
DrawerFooter,
DrawerHeader,
DrawerOverlay,
} from "@chakra-ui/react"
const Demo = () => (
<Drawer isOpen={isOpen} placement="right" onClose={onClose} isFullHeight>
<DrawerOverlay />
<DrawerContent>
<DrawerCloseButton />
<DrawerHeader>Title</DrawerHeader>
<DrawerBody>Content</DrawerBody>
<DrawerFooter>
<Button onClick={onClose}>Close</Button>
</DrawerFooter>
</DrawerContent>
</Drawer>
)
After:
import { Drawer, Portal } from "@chakra-ui/react"
const Demo = () => (
<Drawer.Root
open={isOpen}
placement="end"
onOpenChange={(e) => !e.open && onClose()}
>
<Portal>
<Drawer.Backdrop />
<Drawer.Positioner>
<Drawer.Content height="100%">
<Drawer.CloseTrigger />
<Drawer.Header>Title</Drawer.Header>
<Drawer.Body>Content</Drawer.Body>
<Drawer.Footer>
<Button onClick={onClose}>Close</Button>
</Drawer.Footer>
</Drawer.Content>
</Drawer.Positioner>
</Portal>
</Drawer.Root>
)
Now uses compound components with dot notation. Custom controls use declarative
trigger components instead of the useEditableControls prop-getter pattern.
Component Renaming:
Editable → Editable.RootEditablePreview → Editable.PreviewEditableInput → Editable.InputEditableTextarea → Editable.TextareauseEditableControls → useEditableContextProp Changes:
isDisabled → disabledonChange → onValueChange (receives { value } object)onSubmit → onValueCommitonCancel → onValueRevertstartWithEditView → defaultEditselectAllOnFocus → selectOnFocussubmitOnBlur={false} → submitMode="enter"finalFocusRef → finalFocusEl (function returning element)isPreviewFocusable={false} → add tabIndex={undefined} to
Editable.PreviewBefore:
import { Editable, EditableInput, EditablePreview } from "@chakra-ui/react"
const Demo = () => (
<Editable
defaultValue="Hello"
isDisabled
onSubmit={handleSubmit}
onChange={handleChange}
submitOnBlur={false}
startWithEditView
>
<EditablePreview />
<EditableInput />
</Editable>
)
After:
import { Editable } from "@chakra-ui/react"
const Demo = () => (
<Editable.Root
defaultValue="Hello"
disabled
onValueCommit={handleSubmit}
onValueChange={handleChange}
submitMode="enter"
defaultEdit
>
<Editable.Preview />
<Editable.Input />
</Editable.Root>
)
Custom Controls:
The useEditableControls prop-getter pattern is replaced by declarative trigger
components.
Before:
function EditableControls() {
const { isEditing, getSubmitButtonProps, getCancelButtonProps } =
useEditableControls()
return isEditing ? (
<ButtonGroup size="sm">
<IconButton icon={<CheckIcon />} {...getSubmitButtonProps()} />
<IconButton icon={<CloseIcon />} {...getCancelButtonProps()} />
</ButtonGroup>
) : null
}
After:
<Editable.Control>
<Editable.EditTrigger asChild>
<IconButton variant="ghost" size="xs">
<LuPencilLine />
</IconButton>
</Editable.EditTrigger>
<Editable.CancelTrigger asChild>
<IconButton variant="outline" size="xs">
<LuX />
</IconButton>
</Editable.CancelTrigger>
<Editable.SubmitTrigger asChild>
<IconButton variant="outline" size="xs">
<LuCheck />
</IconButton>
</Editable.SubmitTrigger>
</Editable.Control>
Replaced by Field for standard form controls and Fieldset for grouped
controls (radio groups, checkbox groups). The as='fieldset' pattern is
replaced with a dedicated Fieldset component.
Component Renaming:
FormControl → Field.RootFormLabel → Field.LabelFormHelperText → Field.HelperTextFormErrorMessage → Field.ErrorTextFor fieldset usage:
FormControl as='fieldset' → Fieldset.RootFormLabel as='legend' → Fieldset.LegendFormHelperText → Fieldset.HelperTextFormErrorMessage → Fieldset.ErrorTextProp Changes:
isInvalid → invalidisRequired → requiredisDisabled → disabledisReadOnly → readOnlyBefore:
import {
FormControl,
FormErrorMessage,
FormHelperText,
FormLabel,
} from "@chakra-ui/react"
const Demo = () => (
<FormControl isInvalid={isError}>
<FormLabel>Email</FormLabel>
<FormHelperText>We'll never share your email.</FormHelperText>
<FormErrorMessage>Email is required.</FormErrorMessage>
</FormControl>
)
After:
import { Field } from "@chakra-ui/react"
const Demo = () => (
<Field.Root invalid={isError}>
<Field.Label>Email</Field.Label>
<Field.HelperText>We'll never share your email.</Field.HelperText>
<Field.ErrorText>Email is required.</Field.ErrorText>
</Field.Root>
)
Field.ErrorTextonly renders wheninvalidistrue, so no conditional logic is needed.
Fieldset Usage:
Before:
import { FormControl, FormHelperText, FormLabel } from "@chakra-ui/react"
const Demo = () => (
<FormControl as="fieldset">
<FormLabel as="legend">Favorite Character</FormLabel>
<FormHelperText>Select only if you're a fan.</FormHelperText>
</FormControl>
)
After:
import { Fieldset } from "@chakra-ui/react"
const Demo = () => (
<Fieldset.Root>
<Fieldset.Legend>Favorite Character</Fieldset.Legend>
<Fieldset.HelperText>Select only if you're a fan.</Fieldset.HelperText>
</Fieldset.Root>
)
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>
All transition components have been replaced by a unified Presence component
that uses CSS-based animations instead of JavaScript-based transitions.
Component Mapping:
Fade → Presence with
animationName={{ _open: "fade-in", _closed: "fade-out" }}ScaleFade → Presence with
animationStyle={{ _open: "scale-fade-in", _closed: "scale-fade-out" }}SlideFade → Presence with
animationName={{ _open: "slide-from-bottom, fade-in", _closed: "slide-to-bottom, fade-out" }}Slide → Presence with direction-specific positioning and animationProp Changes:
in → presentinitialScale → removed (scale is fixed in CSS keyframes)offsetX / offsetY → removed (offset is fixed in CSS keyframes)direction → replaced by positioning props and direction-specific animation
namesBefore:
import { Fade, Slide } from "@chakra-ui/react"
const Demo = () => (
<>
<Fade in={isOpen}>
<Box>Fading content</Box>
</Fade>
<Slide direction="bottom" in={isOpen}>
<Box>Sliding content</Box>
</Slide>
</>
)
After:
import { Presence } from "@chakra-ui/react"
const Demo = () => (
<>
<Presence
present={isOpen}
animationName={{ _open: "fade-in", _closed: "fade-out" }}
animationDuration="moderate"
>
<Box>Fading content</Box>
</Presence>
<Presence
present={isOpen}
position="fixed"
bottom="0"
insetX="0"
animationName={{
_open: "slide-from-bottom-full",
_closed: "slide-to-bottom-full",
}}
animationDuration="moderate"
>
<Box>Sliding content</Box>
</Presence>
</>
)
Slide Direction Mapping:
| Direction | Positioning | Open Animation | Close Animation |
|---|---|---|---|
top | position="fixed" top="0" insetX="0" | slide-from-top-full | slide-to-top-full |
bottom | position="fixed" bottom="0" insetX="0" | slide-from-bottom-full | slide-to-bottom-full |
left | position="fixed" left="0" insetY="0" | slide-from-left-full | slide-to-left-full |
right | position="fixed" right="0" insetY="0" | slide-from-right-full | slide-to-right-full |
RangeSlider has been unified with Slider — pass an array value for range
mode. Both now require a Slider.Control wrapper and Slider.HiddenInput
inside each thumb.
Component Renaming:
Slider / RangeSlider → Slider.RootSliderTrack / RangeSliderTrack → Slider.TrackSliderFilledTrack / RangeSliderFilledTrack → Slider.RangeSliderThumb / RangeSliderThumb → Slider.ThumbProp Changes:
onChange → onValueChange (receives { value })onChangeEnd → onValueChangeEnd (receives { value })onChangeStart → removedcolorScheme → colorPaletteisReversed / reversed → removed (use dir="rtl")focusThumbOnChange → removedBefore:
import {
RangeSlider,
RangeSliderFilledTrack,
RangeSliderThumb,
RangeSliderTrack,
} from "@chakra-ui/react"
const Demo = () => (
<RangeSlider defaultValue={[10, 30]} onChange={(val) => console.log(val)}>
<RangeSliderTrack>
<RangeSliderFilledTrack />
</RangeSliderTrack>
<RangeSliderThumb index={0} />
<RangeSliderThumb index={1} />
</RangeSlider>
)
After:
import { Slider } from "@chakra-ui/react"
const Demo = () => (
<Slider.Root
defaultValue={[10, 30]}
onValueChange={(e) => console.log(e.value)}
>
<Slider.Control>
<Slider.Track>
<Slider.Range />
</Slider.Track>
<Slider.Thumb index={0}>
<Slider.HiddenInput />
</Slider.Thumb>
<Slider.Thumb index={1}>
<Slider.HiddenInput />
</Slider.Thumb>
</Slider.Control>
</Slider.Root>
)
TableContainer is now Table.ScrollAreaTd(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>
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.CloseTriggerBefore:
<Tag>
<TagLabel>Green</TagLabel>
<TagCloseButton />
</Tag>
After:
<Tag.Root>
<Tag.Label>Green</Tag.Label>
<Tag.CloseTrigger />
</Tag.Root>
Now uses compound components with dot notation. v3 also introduces
Alert.Content as a wrapper for title and description.
Component Renaming:
Alert → Alert.RootAlertIcon → Alert.IndicatorAlertTitle → Alert.TitleAlertDescription → Alert.DescriptionProp Changes:
addRole prop (role is handled automatically in v3)Before:
import {
Alert,
AlertDescription,
AlertIcon,
AlertTitle,
} from "@chakra-ui/react"
const Demo = () => (
<Alert status="error">
<AlertIcon />
<AlertTitle>Your browser is outdated!</AlertTitle>
<AlertDescription>Your Chakra experience may be degraded.</AlertDescription>
</Alert>
)
After:
import { Alert } from "@chakra-ui/react"
const Demo = () => (
<Alert.Root status="error">
<Alert.Indicator />
<Alert.Content>
<Alert.Title>Your browser is outdated!</Alert.Title>
<Alert.Description>
Your Chakra experience may be degraded.
</Alert.Description>
</Alert.Content>
</Alert.Root>
)
Variant Changes:
The left-accent and top-accent variants have been removed. Replicate them
using border style props on Alert.Root:
left-accent → variant="subtle" + borderStartWidth="3px" +
borderStartColor="colorPalette.solid"top-accent → variant="subtle" + borderTopWidth="3px" +
borderTopColor="colorPalette.solid"New variants surface and outline have been added.
Before:
<Alert status="success" variant="left-accent">
<AlertIcon />
Data uploaded to the server. Fire on!
</Alert>
After:
<Alert.Root
status="success"
variant="subtle"
borderStartWidth="3px"
borderStartColor="colorPalette.solid"
>
<Alert.Indicator />
Data uploaded to the server. Fire on!
</Alert.Root>
startColor and endColor props now use CSS variablesBefore:
<Skeleton startColor="pink.500" endColor="orange.500" />
After:
<Skeleton
css={{
"--start-color": "colors.pink.500",
"--end-color": "colors.orange.500",
}}
/>
isLoaded prop is now loadingBefore:
<Skeleton isLoaded>
<span>Chakra ui is cool</span>
</Skeleton>
After:
<Skeleton loading={false}>
<span>Chakra ui is cool</span>
</Skeleton>
Renamed to Steps with compound component pattern. The useSteps hook is still
available but with updated API.
Component Renaming:
Stepper → Steps.RootStep → Steps.ItemStepIndicator → Steps.IndicatorStepStatus → Steps.StatusStepTitle → Steps.TitleStepDescription → Steps.DescriptionStepSeparator → Steps.SeparatorProp Changes:
index → stepSteps.ListHook Changes:
useSteps({ index }) → useSteps({ defaultStep })useSteps, use Steps.RootProvider with value={stepsApi}
instead of Steps.RootBefore:
import {
Step,
StepIcon,
StepIndicator,
StepNumber,
StepSeparator,
StepStatus,
StepTitle,
Stepper,
} from "@chakra-ui/react"
const Demo = () => (
<Stepper index={1}>
{steps.map((step, index) => (
<Step key={index}>
<StepIndicator>
<StepStatus complete={<StepIcon />} incomplete={<StepNumber />} />
</StepIndicator>
<StepTitle>{step.title}</StepTitle>
<StepSeparator />
</Step>
))}
</Stepper>
)
After:
import { Steps } from "@chakra-ui/react"
const Demo = () => (
<Steps.Root step={1}>
<Steps.List>
{steps.map((step, index) => (
<Steps.Item key={index}>
<Steps.Indicator>
<Steps.Status complete={<StepIcon />} incomplete={<StepNumber />} />
</Steps.Indicator>
<Steps.Title>{step.title}</Steps.Title>
<Steps.Separator />
</Steps.Item>
))}
</Steps.List>
</Steps.Root>
)
Now uses compound components with dot notation.
Component Renaming:
Stat → Stat.RootStatLabel → Stat.LabelStatNumber → Stat.ValueTextStatHelpText → Stat.HelpTextStatArrow type="increase" → Stat.UpIndicatorStatArrow type="decrease" → Stat.DownIndicatorStatGroup → Stat.Root (nest Stat.Root children inside)Before:
import {
Stat,
StatArrow,
StatHelpText,
StatLabel,
StatNumber,
} from "@chakra-ui/react"
const Demo = () => (
<Stat>
<StatLabel>Revenue</StatLabel>
<StatNumber>$45,670</StatNumber>
<StatHelpText>
<StatArrow type="increase" />
12.5%
</StatHelpText>
</Stat>
)
After:
import { Stat } from "@chakra-ui/react"
const Demo = () => (
<Stat.Root>
<Stat.Label>Revenue</Stat.Label>
<Stat.ValueText>$45,670</Stat.ValueText>
<Stat.HelpText>
<Stat.UpIndicator />
12.5%
</Stat.HelpText>
</Stat.Root>
)
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>
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>
Now a snippet component imported from @/components/ui/tooltip instead of
@chakra-ui/react.
Prop Changes:
label → contenthasArrow → showArrowcloseOnEsc → closeOnEscapecloseOnMouseDown → closeOnPointerDownonOpen / onClose → onOpenChange (receives { open })shouldWrapChildren → wrap children in <span> manuallyplacement, gutter, offset, arrowPadding → grouped into positioning
objectRemoved Props: modifiers, motionProps, portalProps, arrowSize,
arrowShadowColor
Before:
import { Tooltip } from "@chakra-ui/react"
;<Tooltip label="Info" hasArrow placement="top" closeOnEsc={false}>
<button>Hover</button>
</Tooltip>
After:
import { Tooltip } from "@/components/ui/tooltip"
;<Tooltip
content="Info"
showArrow
positioning={{ placement: "top" }}
closeOnEscape={false}
>
<button>Hover</button>
</Tooltip>
Now uses compound components with dot notation. All sub-components are
namespaced under Accordion.
Component Renaming:
Accordion → Accordion.RootAccordionItem → Accordion.Item (now requires a value prop)AccordionButton → Accordion.ItemTriggerAccordionIcon → Accordion.ItemIndicatorAccordionPanel → Accordion.ItemContent + Accordion.ItemBodyProp Changes:
allowMultiple → multipleallowToggle → collapsibledefaultIndex → defaultValue (now an array of strings)index → value (now an array of strings)onChange → onValueChangeBefore:
import {
Accordion,
AccordionButton,
AccordionIcon,
AccordionItem,
AccordionPanel,
Box,
} from "@chakra-ui/react"
const Demo = () => (
<Accordion allowToggle>
<AccordionItem>
<h2>
<AccordionButton>
<Box as="span" flex="1" textAlign="left">
Section 1 title
</Box>
<AccordionIcon />
</AccordionButton>
</h2>
<AccordionPanel pb={4}>Lorem ipsum dolor sit amet.</AccordionPanel>
</AccordionItem>
</Accordion>
)
After:
import { Accordion, Box } from "@chakra-ui/react"
const Demo = () => (
<Accordion.Root collapsible>
<Accordion.Item value="section-1">
<h2>
<Accordion.ItemTrigger>
<Box as="span" flex="1" textAlign="left">
Section 1 title
</Box>
<Accordion.ItemIndicator />
</Accordion.ItemTrigger>
</h2>
<Accordion.ItemContent>
<Accordion.ItemBody pb={4}>
Lorem ipsum dolor sit amet.
</Accordion.ItemBody>
</Accordion.ItemContent>
</Accordion.Item>
</Accordion.Root>
)
Render Props → Context:
The AccordionItem render prop pattern ({({ isExpanded }) => ...}) has been
replaced. Use the Accordion.ItemContext component or the
useAccordionItemContext hook instead. The isExpanded property is now
expanded.
Before:
<AccordionItem>
{({ isExpanded }) => (
<>
<AccordionButton>
<Box flex="1" textAlign="left">
Section title
</Box>
{isExpanded ? <MinusIcon /> : <AddIcon />}
</AccordionButton>
<AccordionPanel>Content</AccordionPanel>
</>
)}
</AccordionItem>
After:
<Accordion.Item value="section-1">
<Accordion.ItemContext>
{({ expanded }) => (
<>
<Accordion.ItemTrigger>
<Box flex="1" textAlign="left">
Section title
</Box>
{expanded ? <LuMinus /> : <LuPlus />}
</Accordion.ItemTrigger>
<Accordion.ItemContent>
<Accordion.ItemBody>Content</Accordion.ItemBody>
</Accordion.ItemContent>
</>
)}
</Accordion.ItemContext>
</Accordion.Item>
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 respectivelyBefore:
<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.RootBefore:
<Tabs isLazy />
After:
<Tabs.Root lazyMount unmountOnExit />
Show and Hide components are removed in favor of hideFrom and
hideBelowBefore:
<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>
Refactored to use compound components. The single <Checkbox> is now split into
explicit parts for full control over structure and styling.
Prop Changes:
isChecked → checkedisDisabled → disabledisInvalid → invalidisReadOnly → readOnlyisIndeterminate → checked="indeterminate"onChange → onCheckedChangecolorScheme → colorPaletteicon → render as children of Checkbox.ControliconColor → color on Checkbox.IndicatoriconSize → boxSize on Checkbox.IndicatorisFocusable → removedCheckboxGroup:
isDisabled → disabledonChange → onValueChangeisNative → removedBefore:
import { Checkbox } from "@chakra-ui/react"
const Demo = () => (
<Checkbox
isChecked={checked}
isIndeterminate={indeterminate}
onChange={(e) => setChecked(e.target.checked)}
colorScheme="blue"
>
Accept terms
</Checkbox>
)
After:
import { Checkbox } from "@chakra-ui/react"
const Demo = () => (
<Checkbox.Root
checked={indeterminate ? "indeterminate" : checked}
onCheckedChange={(e) => setChecked(!!e.checked)}
colorPalette="blue"
>
<Checkbox.HiddenInput />
<Checkbox.Control>
<Checkbox.Indicator />
</Checkbox.Control>
<Checkbox.Label>Accept terms</Checkbox.Label>
</Checkbox.Root>
)
Refactored to use compound components. Radio is now RadioGroup.Item with
explicit sub-components: ItemHiddenInput, ItemIndicator, ItemText.
Component Renaming:
RadioGroup → RadioGroup.RootRadio → RadioGroup.Item (with required sub-components)RadioGroup Prop Changes:
onChange → onValueChange (receives { value } object)colorScheme → colorPaletteRadio Prop Changes:
isDisabled → disabledisInvalid, isChecked, defaultChecked → removed (controlled from Root)colorScheme → removed from items (set colorPalette on Root instead)inputProps → spread into RadioGroup.ItemHiddenInputBefore:
import { Radio, RadioGroup } from "@chakra-ui/react"
const Demo = () => (
<RadioGroup defaultValue="2" onChange={(val) => setValue(val)}>
<Radio value="1">Option 1</Radio>
<Radio value="2">Option 2</Radio>
</RadioGroup>
)
After:
import { RadioGroup } from "@chakra-ui/react"
const Demo = () => (
<RadioGroup.Root defaultValue="2" onValueChange={(e) => setValue(e.value)}>
<RadioGroup.Item value="1">
<RadioGroup.ItemHiddenInput />
<RadioGroup.ItemIndicator />
<RadioGroup.ItemText>Option 1</RadioGroup.ItemText>
</RadioGroup.Item>
<RadioGroup.Item value="2">
<RadioGroup.ItemHiddenInput />
<RadioGroup.ItemIndicator />
<RadioGroup.ItemText>Option 2</RadioGroup.ItemText>
</RadioGroup.Item>
</RadioGroup.Root>
)
isActive → data-active attributeisDisabled → disabledisLoading → loadingleftIcon and rightIcon → passed as childreniconSpacing → removed (use gap in flex layout)colorScheme → colorPaletteExample:
// 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>
isDisabled → disabledisInvalid → invalidisReadOnly → readOnlyisRequired → requiredcolorScheme → colorPalettefocusBorderColor → use CSS variableserrorBorderColor → use CSS variablesExample:
// 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"
}}
/>
isChecked → checkedisDisabled → disabledisInvalid → invalidisIndeterminate → checked="indeterminate"onChange → onCheckedChangecolorScheme → colorPaletteiconColor → color on Checkbox.IndicatoriconSize → boxSize on Checkbox.IndicatorisFocusable → removedisOpen → openonClose → onOpenChange (receives { open })isCentered → placement="center"closeOnOverlayClick → closeOnInteractOutsidecloseOnEsc → closeOnEscapeblockScrollOnMount → preventScrollonOverlayClick → onInteractOutsideonEsc → onEscapeKeyDownonCloseComplete → onExitCompleteinitialFocusRef → initialFocusEl (function returning element)finalFocusRef → finalFocusEl (function returning element)scrollBehavior → unchangedmotionPreset → unchangedtrapFocus → unchanged2xl–6xl → mapped to xlallowPinchZoom, lockFocusAcrossFrames, preserveScrollBarGap,
returnFocusOnClose, useInert, portalPropsspacing → gapdivider → separatorExample:
// 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>