v9.0.0: Chakra v3 Migration Guide
Breaking changes when upgrading to the Chakra v3 version of Casper UI
Casper UI has upgraded its underlying Chakra UI dependency from v2 to v3. Chakra v3 was a near-complete rewrite with many breaking API changes that could not be shimmed transparently. This guide covers every consumer-facing breaking change, what to do, and why it was necessary. Please refer to the individual component documentation for more details and examples on how to use the new APIs.
There may be additional breaking changes not listed here that are specific to your usage. Refer to the (also incomplete) Chakra UI v3 migration guide for a list of changes in the underlying library.
Here is an example PR that upgrades Casper UI from 8.x.x to 9.0.0, which you can use as a reference for your own migration: https://github.com/Caspeco/CustomerConnectService/pull/803
Quick Reference
Section titled “Quick Reference”| Component / API | Change | Section |
|---|---|---|
<ThemeColorModeScript /> |
Removed, dark mode works without it | Global Changes |
as prop |
Prefer asChild pattern |
Global Changes |
ThemeProviderContext |
Removed | Global Changes |
<Icon icon={Icons.X} /> |
Import individual icon components | Fully Rewritten |
useToast |
Replaced by showToast + <Toaster /> |
Fully Rewritten |
Radio |
Group and value required, RadioAddon, useRadio removed |
Fully Rewritten |
isInvalid on inputs |
Must be set on FormControl |
Form Wrappers |
FormControl as="fieldset" |
Replaced by <Fieldset> |
Form Wrappers |
Accordion index props |
defaultIndex/index → defaultValue/value |
Value-based APIs |
Tabs index/change props |
defaultIndex/index/onChange renamed |
Value-based APIs |
MenuItem / MenuItemGroup |
value now required, new onSelect |
Value-based APIs |
Checkbox / Switch |
onChange → onCheckedChange |
Prop Renames & Removals |
Combobox |
Size values corrected, selectedItems required |
Prop Renames & Removals |
Button card variant |
card variant and isSelected removed |
Prop Renames & Removals |
Image fallback |
Fallback props removed | Prop Renames & Removals |
Skeleton colors |
startColor/endColor removed |
Prop Renames & Removals |
Drawer / Modal |
autoFocus removed (always true) |
Prop Renames & Removals |
Global Changes
Section titled “Global Changes”These affect every consumer and should be addressed first.
<ThemeColorModeScript /> removed
Section titled “<ThemeColorModeScript /> removed”Why?
Chakra v3 uses localStorage and a CSS class on the <html> element to persist color mode. The inline script that prevented flash-of-wrong-color-mode is no longer needed as Chakra handles this natively.
Remove any usage of <ThemeColorModeScript /> from your app’s root or HTML template.
For instructions on reading and toggling dark mode, see the updated Dark Mode guide.
as prop replaced by asChild
Section titled “as prop replaced by asChild”Why?
Chakra v3 adopted the Radix asChild pattern for
polymorphic rendering. It provides better TypeScript support and avoids prop merging issues that the as
approach suffered from.
The as prop for polymorphic rendering still exists in some components but may cause TypeScript errors. Migrate to the asChild pattern instead.
<Link as={ReactRouterLink} to="/home" color="red">Home</Link><Link asChild color="red"> <ReactRouterLink to="/home">Home</ReactRouterLink></Link>ThemeProviderContext removed
Section titled “ThemeProviderContext removed”Why?
This was a React Context with no documented usage, so for simplicity, it was removed from the library
exports.
ThemeProviderContext has been removed from the library exports.
Fully Rewritten Components
Section titled “Fully Rewritten Components”These components have changed significantly and require the most migration effort.
Why?
As part of the migration, we found that importing any component from the library caused the entire icon set
to be included in the bundle due to all of our icons being exported from a single enum. For this reason, we
switched to individual icon components that are tree-shakeable, which should reduce your bundle size by up
to 300kb.
The <Icon> component and Icons enum are removed. Import individual icon components by name instead.
import { Icon, Icons, ThemeIconSize } from "@caspeco/casper-ui-library";import { AbsenceIcon, ThemeIconSize } from "@caspeco/casper-ui-library";
<Icon icon={Icons.Absence} size={ThemeIconSize.Large} /><AbsenceIcon size={ThemeIconSize.Large} />For components that accept an icon prop, you can now pass the icon component directly rather than using the enum:
<Tag icon={Icons.Absence}>Away</Tag><Tag icon={AbsenceIcon}>Away</Tag>All icon names follow the pattern <Name>Icon. For a full list of available icons, see the Icon component docs.
Why?
Chakra v3 rewrote their toast API which we were exporting directly. It was not feasible to shim the old API
on top of the new one.
The useToast hook is removed. Import showToast directly and render <Toaster /> once somewhere in your app (e.g., in your root layout). See the Toaster docs for full usage.
Before:
import { useToast } from "@caspeco/casper-ui-library";
function MyComponent() { const { showToast } = useToast();
return ( <Button onClick={() => showToast({ type: "success", title: "Saved", description: "Your changes have been saved." }) } > Save </Button> );}After:
// In your root layout — render onceimport { Toaster } from "@caspeco/casper-ui-library";
function App() { return ( <> <MyApp /> <Toaster /> </> );}// In any component — call showToast directlyimport { showToast } from "@caspeco/casper-ui-library";
function MyComponent() { return ( <Button onClick={() => showToast({ type: "success", title: "Saved", description: "Your changes have been saved." }) } > Save </Button> );}Why?
Lots of things changed internally for radios in Chakra v3 which made migration difficult, mainly due to the
removal of the useRadio hook. To make the migration easier for our different radio variants, we combined
Radio and RadioAddon into a single Radio, letting you control the variant on the parent
RadioGroup.
Radios can no longer be rendered outside of their group.
<Radio isChecked>Option A</Radio><RadioGroup defaultValue="a"> <Radio value="a">Option A</Radio></RadioGroup>The RadioAddon component has been removed. To achieve the same styling, use the addon variant on RadioGroup:
<RadioAddon isChecked value="1">Radio addon</RadioAddon><RadioGroup defaultValue="1" variant="addon"> <Radio value="1">Radio addon</Radio></RadioGroup>RadioGroup’s onChange has been renamed to onValueChange.
<RadioGroup onChange={(val) => setActiveValue(val)} value={activeValue}><RadioGroup onValueChange={({ value }) => setActiveValue(value)} value={activeValue}> <Radio isChecked={value === "a"} value="a">Option A</Radio> <Radio value="a">Option A</Radio></RadioGroup>The useRadio hook has been removed. The custom hook pattern required consumers to manage radio group state manually. The new RadioCardGroup handles this automatically for RadioCard components,
and the RadioGroup handles state management for Radio components. A common custom layout with Addon Radios and flexDirection="row-reverse" can be found in the Radio docs.
Before:
import { RadioCard, Stack, useRadioGroup } from "@caspeco/casper-ui-library";
export default function Demo() { const sizes = ["small", "medium", "large"];
// Hook that will provide all the state management logic for our group of radio cards const { getRootProps, getRadioProps } = useRadioGroup({ name: "size-selection", defaultValue: "medium", onChange: console.log, });
// Get the props of the radio root to spread onto our custom radio group element const radioGroupProps = getRootProps(); return ( <Stack {...radioGroupProps}> {sizes.map((size) => { // Pass value to get the radio element props to be spread onto our radio card const radioProps = getRadioProps({ value: size }); return ( <RadioCard key={size} {...radioProps}> {size} </RadioCard> ); })} </Stack> );}After:
import { RadioCard, RadioCardGroup } from "@caspeco/casper-ui-library";
export default function Demo() { const sizes = ["small", "medium", "large"];
return ( <RadioCardGroup defaultValue="medium" onValueChange={console.log}> {sizes.map((size) => { return ( <RadioCard key={size} value={size}> {size} </RadioCard> ); })} </RadioCardGroup> );}Several props have been removed from Radio and RadioCard: isFocusable, isChecked, isReadOnly, and isRequired.
You can set these on the respective RadioGroup instead, or wrap in a FormControl / Fieldset for isReadOnly and isRequired.
The value prop is now required as this is how the radio group tracks which item is selected.
To set a default selection, use defaultValue on RadioCardGroup instead of defaultChecked on individual items.
See the Radio docs and RadioCard docs for full usage.
Form Wrappers
Section titled “Form Wrappers”Form/Input Validation (isInvalid and isRequired)
Section titled “Form/Input Validation (isInvalid and isRequired)”Why?
Chakra removed the support for isInvalid and isRequired on individual inputs. FormControl makes it
easier to manage form state and ensures that validation styles are applied consistently to the entire field
(label, helper text, error message) rather than just the input element.
isInvalid and isRequired have been removed from Input, Textarea, Select, and Combobox. These must now be set on the wrapping FormControl instead.
<Input isInvalid /><Textarea isInvalid /><Select isInvalid />
<FormControl isInvalid> <Input /></FormControl>The same applies to isRequired:
<Input isRequired /><FormControl isRequired> <Input /></FormControl>isDisabled is still supported on individual inputs, but can also be set on FormControl. One change with this behavior is that if you wrap an input with FormControl,
it is no longer possible to disable the input by setting isDisabled on the input itself. You must set it on the FormControl instead.
<FormControl> <Input isDisabled /></FormControl><FormControl isDisabled> <Input /></FormControl>Fieldset
Section titled “Fieldset”Why?
Chakra v3 introduced a dedicated Fieldset compound component. The polymorphic as prop approach was
removed as part of the broader as → asChild migration.
FormControl no longer accepts as="fieldset". Use the new Fieldset component instead.
<FormControl as="fieldset"> <FormLabel as="legend">Options</FormLabel> {/* ... */}</FormControl>
<Fieldset.Root> <Fieldset.Legend>Options</Fieldset.Legend> <Fieldset.Content> {/* ... */} </Fieldset.Content></Fieldset.Root>Value-based API Changes
Section titled “Value-based API Changes”Chakra v3 standardized index-based APIs to use string value identifiers across all components. This affects Accordion, Tabs, Menu, and RadioGroup.
Accordion
Section titled “Accordion”Why?
Using the previous index-based API was messy to shim.
defaultIndex and index are renamed to defaultValue and value. Each AccordionItem now requires an explicit value prop. See the Accordion docs for full usage.
<Accordion defaultIndex={[0]}> <AccordionItem> <AccordionButton>Section 1</AccordionButton><Accordion defaultValue={["section-1"]}> <AccordionItem value="section-1"> <AccordionButton>Section 1</AccordionButton> <AccordionPanel>Content</AccordionPanel> </AccordionItem></Accordion>Why?
Using the previous index-based API was messy to shim. Image fallbacks are removed (see Image).
defaultIndex, index, and onChange are renamed to defaultValue, value, and onValueChange. Each Tab and TabPanel now requires a value prop. imgFallbackColor on TabImage is also removed. See the Tabs docs for full usage.
<Tabs defaultIndex={0} onChange={(index) => console.log(index)}><Tabs defaultValue="tab-1" onValueChange={(value) => console.log(value)}> <TabList> <Tab>First</Tab> <Tab>Second</Tab> <Tab value="tab-1">First</Tab> <Tab value="tab-2">Second</Tab> </TabList> <TabPanels> <TabPanel>Content 1</TabPanel> <TabPanel>Content 2</TabPanel> <TabPanel value="tab-1">Content 1</TabPanel> <TabPanel value="tab-2">Content 2</TabPanel> </TabPanels></Tabs>Why?
Using the previous index-based API was messy to shim. closeOnBlur was removed because the behavior is now
always-on and not configurable.
MenuItem and MenuItemGroup now require a value prop. A new onSelect callback on the parent Menu replaces per-item onClick handlers. closeOnBlur has also been removed. See the Menu docs for full usage.
<Menu closeOnBlur> <MenuList> <MenuItem onClick={() => handleSelect("profile")}>Profile</MenuItem> <MenuItem onClick={() => handleSelect("settings")}>Settings</MenuItem> </MenuList></Menu>
<Menu onSelect={(value) => handleSelect(value)}> <MenuList> <MenuItem value="profile">Profile</MenuItem> <MenuItem value="settings">Settings</MenuItem> </MenuList></Menu>Prop Renames & Removals
Section titled “Prop Renames & Removals”Checkbox
Section titled “Checkbox”Why?
Chakra v3 standardized checkbox change events to pass a details object (consistent with their switch and
other toggle components) rather than a plain DOM event.
onChange has been renamed to onCheckedChange. The callback signature has also changed — it now receives a CheckedChangeDetails object which supports “indeterminate” state. This same rename applies to Switch. See the Checkbox docs for full usage.
<Checkbox onChange={(e) => setChecked(e.target.checked)}>Label</Checkbox><Checkbox onCheckedChange={({ checked }) => setChecked(Boolean(checked))}>Label</Checkbox>Combobox
Section titled “Combobox”Why?
The previous size values (s, m, l) were incorrect and inconsistent with the rest of the component
library. selectedItems was already required for the component to function correctly, the type now enforces
this.
The size prop values have been corrected to match the standard Chakra size scale (s → sm, m → md, etc.). selectedItems is now required. See the Combobox docs for full usage.
<Combobox size="s" items={items} /><Combobox size="m" items={items} /><Combobox size="l" items={items} />
<Combobox size="sm" items={items} selectedItems={selected} /><Combobox size="md" items={items} selectedItems={selected} /><Combobox size="lg" items={items} selectedItems={selected} />Button
Section titled “Button”Why?
This was deprecated for a long time, and this is a good opportunity for a clean break.
The card variant and its companion isSelected prop have been removed.
<Button variant="card" isSelected={isActive}>Option</Button>If you were using this for a selectable card UI, consider using the RadioCard component instead.
Why?
Chakra v3 removed its built-in image fallback system. The feature can be replicated with native browser
APIs.
The fallback-related props have been removed: fallback, fallbackSrc, fallbackStrategy, and ignoreFallback. See the Image docs for full usage.
<Image src={src} fallbackSrc="/placeholder.png" />
<Image src={src} onError={(e) => { (e.target as HTMLImageElement).src = "/placeholder.png"; }}/>Skeleton
Section titled “Skeleton”Why?
Chakra v3 simplified the skeleton animation to an opacity fade.
startColor and endColor have been removed. Use backgroundColor to change the skeleton color. The animation now works by fading opacity rather than transitioning between two colors. See the Skeleton docs for full usage.
<Skeleton startColor={ThemeColor.Accent} endColor={ThemeColor.OnAccent} /><Skeleton backgroundColor={ThemeColor.Accent} />Drawer and Modal
Section titled “Drawer and Modal”Why?
Chakra v3 removed this option.
The autoFocus prop has been removed from both Drawer and Modal. Focus is now always trapped and set automatically when the overlay opens.
<Drawer autoFocus={false}><Drawer>