Migrating from @cfa/react-components
This guide explains how to migrate from @cfa/react-components to @cfa/react-core.
Migration Strategy
Before diving in, a few recommendations from teams that have been through this migration:
Consider component-by-component over page-by-page migration. Tackling one component type at a time (e.g., all TextInput replacements across the app in one pass) can be easier to review and test than doing all components on a single page and moving on. This also ensures you migrate off all instances across your codebase.
Both libraries can coexist. You don’t have to migrate everything at once. @cfa/react-components and @cfa/react-core can run side-by-side without issues/conflicts. Migrate in deliberate batches and be explicit about which components are done vs. still pending.
Write unit tests alongside each swap. Prop renames and event shape changes are subtle — visual QA alone will miss them. Write tests as you migrate each component type.
Budget time for CSS. React-core’s default styles, DOM structure, and class naming conventions differ significantly from react-components. Expect to write global CSS overrides to align visual output with your existing design. See CSS Considerations below.
Summary of Changes
| @cfa/react-components | @cfa/react-core | What Changed | Notes |
|---|---|---|---|
Dropdown | Select | Rename + API Change | Replace all Dropdown imports/usages with Select |
Multiselect | Select | Rename + API Change | Use <Select.Root selectionMode="multiple"> |
SearchDropdown | Combobox | Rename + API Change | New component with different state management approach |
TextField | TextInput / TextAreaInput | Rename + API Change | Multiline fields use TextAreaInput; single-line use TextInput |
RadioButton | Radio | Rename + API Change | Label is now passed as children, not a prop |
RadioGroup | RadioGroup | API Change | Same name; props changed: isDisabled, isRequired, isInvalid, errorMessage (not errorText) |
Checkbox | Checkbox | API Change | Same name; checked → isSelected, onChange(event) → onChange(boolean) |
DatePicker | DatePicker | Stabilized | Moved from UNSTABLE_DatePicker to DatePicker (stable as of v1.0.2) |
TimePicker | — | No direct replacement | Rebuild as a Select-based dropdown with a custom time options utility |
InputLabel | Label | Rename | Simply renamed; or use the label prop built into most react-core form components |
HelperText | Text with slot="description" | Removed | Use Text component with slot prop instead |
Icon | Direct icon imports | Removed | Import icons directly from respective icon packages |
List | ListBox / GridList | Split | Use ListBox for static items, GridList for interactive elements |
Logo | Direct logo imports | Package Change | Import from @cfa/brand-icons/logos |
Tag | Chip / TagGroup | Rename + API Change | Chip is display-only; use TagGroup with onRemove for dismissible tags |
Typography | Heading + HTML elements | Deprecated | Use Heading for headlines, HTML elements for body text (<span>, <p>) |
ButtonGroup | — | Deprecated | Use custom layout with multiple buttons |
FormGroup | CheckboxGroup / RadioGroup | Replaced | Use dedicated group components |
StatusIndicator | — | Deprecated | No direct replacement; use a custom styled element until a dedicated component is available |
LocalNavigation | — | Deprecated | Can be composed with existing react-core components |
IconButtonGroup | Multiple IconButton | Deprecated | Use IconButton’s in flex/grid layout |
useMediaQuery | — | Deprecated | Use CSS media queries |
useBreakpoints | — | Deprecated | Use CSS media queries |
Prop API Changes
One of the most pervasive changes is a consistent renaming of props across react-core components. Get familiar with these patterns early — they appear in nearly every component:
| @cfa/react-components | @cfa/react-core |
|---|---|
disabled | isDisabled |
required | isRequired |
checked / value (boolean) | isSelected |
errorText | isInvalid + <Component.Error> |
helperText | description or <Component.Description> |
label (as prop) | Children or <Component.Label> |
onChange(event) | onChange(value) — value directly, not event |
Major Component Changes
Dropdown
Change Type: Rename + API Change
Action Required: Replace all Dropdown usages with Select
The Dropdown has been deprecated and replaced with Select. The new component gives more control to engineering teams over state management and render items in any way that you wish.
What used to be a single <Dropdown> with many props becomes a <Select.Root> tree with nested sub-components (Select.Label, Select.Button, Select.Value, Select.Popover, Select.List, Select.Item). The JSX is more verbose, but significantly more flexible.
Before
import { Dropdown } from "@cfa/react-components";
const MyComponent = () => ( <Dropdown label="Item" onChange={() => {}} options={["Option 1", "Option 2", "Option 3"]} placeholder="Select an item" value={null} />);After
import { Select } from "@cfa/react-core";
const MyComponent = () => ( <Select.Root> <Select.Label>Item</Select.Label> <Select.Button> <Select.Value /> </Select.Button> <Select.Popover> <Select.List> <Select.Item id="1">Item 1</Select.Item> <Select.Item id="2">Item 2</Select.Item> <Select.Item id="3">Item 3</Select.Item> </Select.List> </Select.Popover> </Select.Root>);Multiselect
Change Type: Rename + API Change
Action Required: Replace Multiselect with Select using selectionMode="multiple"
Multiselect is replaced by the same Select compound component as Dropdown, with selectionMode="multiple" on the root.
Before
import { Multiselect } from "@cfa/react-components";
const MyComponent = () => ( <Multiselect label="Items" onChange={() => {}} options={["Option 1", "Option 2", "Option 3"]} value={[]} />);After
import { Select } from "@cfa/react-core";
const MyComponent = () => ( <Select.Root selectionMode="multiple"> <Select.Label>Items</Select.Label> <Select.Button> <Select.Value /> </Select.Button> <Select.Popover> <Select.List> <Select.Item id="1">Item 1</Select.Item> <Select.Item id="2">Item 2</Select.Item> <Select.Item id="3">Item 3</Select.Item> </Select.List> </Select.Popover> </Select.Root>);SearchDropdown
Change Type: Rename + API Change Action Required: Update component usage and state management approach
The SearchDropdown has been deprecated and replaced with the Combobox. The new component gives more control to engineering teams over state management. This component allows you to pass in your list of items and render them in any way that you wish.
You can choose to load all items into the Combobox on initial render, or dynamically load them as the user types.
Before
import { SearchDropdown } from "@cfa/react-components";
const MyComponent = () => ( <SearchDropdown label="Select an item" onChange={() =>{}} value={// item} onSearch={async (query, offset, cursor) => { bool, results, cursor }} renderOption={(option) => ({ id: option.id, textValue: option.value, rendered: option, })} />);After
import { Combobox } from "@cfa/react-core";
const MyComponent = () => { return ( <Combobox.Root> <Combobox.Label>Search</Combobox.Label> <Combobox.Input description="Type to search options" /> <Combobox.Button iconVariant="search" /> <Combobox.Popover> <Combobox.List> <Combobox.Item id="1">Item 1</Combobox.Item> <Combobox.Item id="2">Item 2</Combobox.Item> <Combobox.Item id="3">Item 3</Combobox.Item> </Combobox.List> </Combobox.Popover> </Combobox.Root> );};TextField
Change Type: Rename + API Change
Action Required: Replace TextField with TextInput (single-line) or TextAreaInput (multiline)
Before
import { TextField } from "@cfa/react-components";
const MyComponent = () => ( <div> <TextField label="Name" onChange={() => {}} /> <TextField label="Notes" multiline onChange={() => {}} /> </div>);After
import { TextInput, TextAreaInput } from "@cfa/react-core";
const MyComponent = () => ( <div> <TextInput label="Name" onChange={(value) => {}} /> <TextAreaInput label="Notes" onChange={(value) => {}} /> </div>);RadioButton / RadioGroup
Change Type: Rename + API Change
Action Required: Replace RadioButton with Radio; update RadioGroup props
RadioButton is renamed to Radio. The label is now passed as children rather than a prop. RadioGroup keeps its name but has updated props — isDisabled, isRequired, isInvalid, and errorMessage replace the old equivalents.
Note that onChange in RadioGroup now receives the value directly (a string), not an event — so remove any event.target.value calls.
Before
import { RadioButton, RadioGroup } from "@cfa/react-components";
const MyComponent = () => ( <RadioGroup label="Choose one" onChange={(event) => console.log(event.target.value)} > <RadioButton label="Choice A" value="a" /> <RadioButton label="Choice B" value="b" /> </RadioGroup>);After
import { Radio, RadioGroup } from "@cfa/react-core";
const MyComponent = () => ( <RadioGroup label="Choose one" onChange={(value) => console.log(value)}> <Radio value="a">Choice A</Radio> <Radio value="b">Choice B</Radio> </RadioGroup>);Checkbox
Change Type: API Change
Action Required: Update checked to isSelected, update onChange handler signature
Checkbox keeps its name, but the API has changed. checked becomes isSelected, and onChange now receives a boolean directly instead of a change event.
Before
import { Checkbox } from "@cfa/react-components";
const MyComponent = () => ( <Checkbox checked={isChecked} onChange={(event) => setIsChecked(event.target.checked)} > Accept terms </Checkbox>);After
import { Checkbox } from "@cfa/react-core";
const MyComponent = () => ( <Checkbox isSelected={isChecked} onChange={(isSelected) => setIsChecked(isSelected)} > Accept terms </Checkbox>);InputLabel
Change Type: Rename
Action Required: Replace all InputLabel imports and usages with Label
The Label primitive component can be used standalone, or is rendered by defining the label prop in TextInput component.
Before
import { InputLabel } from "@cfa/react-components";
const MyComponent = () => <InputLabel>My Input Label</InputLabel>;After
import { Label } from "@cfa/react-core";
const MyComponent = () => <Label>My Input Label</Label>;
// or
import { TextInput } from "@cfa/react-core";
const MyComponent = () => <TextInput label="Label" />;HelperText
Change Type: Removed
Action Required: Replace HelperText with Text component using slot="description"
HelperText has been removed in favor of using native HTML elements or our Text component.
Our Text component is a primitive that is used internally with all form inputs.
Text uses the slot="description" to style it similarly to the HelperText component. This slot is primarily used when nesting this component within a form input, but it also works on its own.
Before
import { HelperText } from "@cfa/react-components";
const MyComponent = () => ( <HelperText helperText="Helper text"/> <HelperText successText="Success!"/> <HelperText errorText="Error!"/>);After
import { Text } from "@cfa/react-core";
const MyComponent = () => ( <div> <Text slot="description">Standard helper text</Text> <Text slot="description" isSuccess> Success message </Text> <Text slot="description" isInvalid> Error message </Text> </div>);
// or
import { TextInput } from "@cfa/react-core";
const MyComponent = () => ( <div> <TextInput description="Description" /> <TextInput description="Success!" isSuccess /> <TextInput errorMessage="Error!" /> </div>);Icon
Change Type: Removed Action Required: Import icons directly from icon packages instead of wrapping in Icon component
The Icon component has been removed in favor of using the Icon components directly.
Before
import { Icon } from "@cfa/react-components";import { Bell } from "@cfa/system-icons";
const App = () => <Icon icon={Bell} size="lg" />;After
import { Bell } from "@cfa/system-icons";
const App = () => <Bell size="lg" />;List
Change Type: Split into Two Components Action Required: Choose appropriate component based on content type
The List component is now available as ListBox and GridList:
ListBoxshould be used for static text elementsGridListis used when you need other clickable or interactive elements within the list item
Before
import { List } from "@cfa/react-components";
const MyComponent = () => ( <List variant="default"> <ListItem>Item 1</ListItem> <ListItem>Item 2</ListItem> <ListItem>Item 3</ListItem> </List>);After (ListBox)
import { ListBox } from "@cfa/react-core";
const MyComponent = () => ( <ListBox.Root onSelectionChange={() => {}} selectedKeys={[ ... ]} selectionMode="single" > <ListBox.Item id="1"> Item 1 </ListBox.Item> <ListBox.Item id="2"> Item 2 </ListBox.Item> <ListBox.Item id="3"> Item 3 </ListBox.Item> </ListBox.Root>);After (GridList)
import { GridList } from "@cfa/react-core";
const MyComponent = () => ( <GridList.Root items={[ { id: '1', name: 'Item 1'}, { id: '2', name: 'Item 2'}, { id: '3', name: 'Item 3'} ]} onSelectionChange={() => {}} selectedKeys={[ ... ]} selectionMode="single" > {(item) => <GridList.Item id={item.id}>{item.name}</GridList.Item>} </GridList.Root>);Logo
Change Type: Package Change
Action Required: Import logos directly from @cfa/brand-icons/logos package
The Logo components are now part of @cfa/brand-icons:
Before
import { Logo } from "@cfa/react-components";
const MyComponent = () => <Logo logo="symbol" size="sm" />;After
import { Symbol } from "@cfa/brand-icons/logos";
const MyComponent = () => <Symbol size="sm" />;Tag
Change Type: Rename + API Change
Action Required: Replace Tag with Chip for display; use TagGroup for dismissible tags
The Tag has been renamed to Chip in react-core, but there are several important differences.
The Chip is used for data display purposes only—it does not have an onClick handler.
For dismiss functionality, use the TagGroup component. TagGroup.Root exposes an onRemove prop that receives the set of removed item keys.
Before
import { Tag } from "@cfa/react-components";
const MyComponent = () => ( <div> <Tag>Display only</Tag> <Tag onDismiss={() => console.log("dismissed")}>Dismissible</Tag> </div>);After
import { Chip, TagGroup } from "@cfa/react-core";
// Display onlyconst DisplayExample = () => <Chip>Display only</Chip>;
// Dismissible — use TagGroup with onRemoveconst DismissExample = () => { const [items, setItems] = React.useState([ { id: 1, name: "First Tag" }, { id: 2, name: "Second Tag" }, ]);
return ( <TagGroup.Root onRemove={(keys) => setItems((prev) => prev.filter((item) => !keys.has(item.id)))}> <TagGroup.List items={items}> {(item) => <TagGroup.Tag>{item.name}</TagGroup.Tag>} </TagGroup.List> </TagGroup.Root> );};Typography
Change Type: Deprecated
Action Required: Use Heading component for headlines and appropriate HTML elements for body text
The Typography component has been deprecated in favor of the Heading component with the headline, overline, and subtitle variants.
We are currently evaluating our typography system. For other styles, we encourage adopters to use the appropriate HTML elements for their use case and apply the styles in their applications.
Before
import { Typography } from "@cfa/react-components";
const MyComponent = () => ( <div> <Typography variant="headline">Page Title</Typography> <Typography variant="overline">Section Header</Typography> <Typography variant="subtitle">Subtitle Text</Typography>
<Typography variant="body1">Body text content</Typography> <Typography variant="body2">Smaller body text</Typography> <Typography variant="caption1">Caption text</Typography> </div>);After
import { Heading } from "@cfa/react-core";
const MyComponent = () => ( <div> <Heading variant="headline1">Page Title</Heading> <Heading variant="overline1">Section Header</Heading> <Heading variant="subtitle1">Subtitle Text</Heading>
{/* Use HTML elements with custom styles for body text */} <p style={{ fontSize: "16px", fontWeight: 400 }}>Body text content</p> <p style={{ fontSize: "14px", fontWeight: 400 }}>Smaller body text</p> <span style={{ fontSize: "12px", fontWeight: 500 }}>Caption text</span> </div>);Typography Style Reference:
body1:font-size: 16px; font-weight: 400body2:font-size: 14px; font-weight: 400caption1:font-size: 12px; font-weight: 500caption2:font-size: 10px; font-weight: 500(NOT recommended for use)
CSS properties like font-family, text-transform, and line-height can be inherited from top-level styles. You should normally not need to set these.
Date/Time
Change Type: Work in Progress Action Required: Use experimental Date/Time components with caution
TimeInput is currently being used internally and will be available in an upcoming release.
Before
import { DatePicker } from "@cfa/react-components";
const MyComponent = () => ( <DatePicker defaultValue={new Date("2024-10-22T04:00:00.000Z")} helperText="Enter date of birth" label="Date of Birth" locale="en-US" />);After
import { DatePicker } from "@cfa/react-core";
const MyComponent = () => ( <DatePicker description="Enter date of birth" label="Date of Birth" name="dob" />);TimePicker
Change Type: No direct replacement
Action Required: Rebuild as a Select-based dropdown with a custom time options utility
There is no TimePicker equivalent in react-core. The recommended approach is to build a Select dropdown populated by a custom generateTimeOptions() utility function that produces the required time intervals.
Error & Validation Handling
Error handling in react-core uses flat props on TextInput (and most other form components):
isInvalid— controls the visual error state (red border, etc.)errorMessage— the error message string rendered when invaliddescription— helper text shown when the field is valid (hidden automatically whenisInvalidis true)
These replace the single errorText and helperText props from react-components.
Before
import { TextInput } from "@cfa/react-components";
const MyComponent = () => ( <TextInput label="Email" errorText="Please enter a valid email" helperText="We'll never share your email" />);After
import { TextInput } from "@cfa/react-core";
const MyComponent = () => ( <TextInput label="Email" description="We'll never share your email" errorMessage="Please enter a valid email" isInvalid={hasError} />);Component Deprecations
ButtonGroup
Change Type: Deprecated Action Required: Use custom layout with multiple Button components
This component has been deprecated.
Before
import { Button, ButtonGroup } from "@cfa/react-components";
const MyComponent = () => ( <ButtonGroup color="primary"> <React.Fragment> <Button>Button 1</Button> <Button>Button 2</Button> <Button>Button 3</Button> </React.Fragment> </ButtonGroup>);After
import { Button } from "@cfa/react-core";
const MyComponent = () => ( <div style={{ display: "flex", gap: "8px" }}> <Button color="primary">Button 1</Button> <Button color="primary">Button 2</Button> <Button color="primary">Button 3</Button> </div>);FormGroup
Change Type: Deprecated Action Required: Use dedicated form group components
This component has been deprecated in favor of dedicated group components like CheckboxGroup and RadioGroup.
Before
import { Checkbox, FormGroup, Radio } from "@cfa/react-components";
const MyComponent = () => ( <div> <FormGroup label="Select options"> <Checkbox>Option 1</Checkbox> <Checkbox>Option 2</Checkbox> <Checkbox>Option 3</Checkbox> </FormGroup>
<FormGroup label="Choose one"> <Radio name="choice">Choice A</Radio> <Radio name="choice">Choice B</Radio> </FormGroup> </div>);After
import { Checkbox, CheckboxGroup, Radio, RadioGroup } from "@cfa/react-core";
const MyComponent = () => ( <div> <CheckboxGroup label="Select options"> <Checkbox value="option1">Option 1</Checkbox> <Checkbox value="option2">Option 2</Checkbox> <Checkbox value="option3">Option 3</Checkbox> </CheckboxGroup>
<RadioGroup label="Choose one"> <Radio value="optionA">Option A</Radio> <Radio value="optionB">Option B</Radio> </RadioGroup> </div>);StatusIndicator
Change Type: Deprecated Action Required: No direct replacement — use a custom styled element
This component has been deprecated. There were no actual usages of this component in GitHub, and a more flexible alternative is planned for the future.
Badge from react-core is not a suitable replacement — it is an overlay component designed to position a count badge on top of another element, and requires the badgeContent prop. It cannot be used as a standalone colored dot indicator.
Until a dedicated component is available, use a plain element with CSS:
Before
import { StatusIndicator } from "@cfa/react-components";
const MyComponent = () => <StatusIndicator status="success" />;After
{ /* No direct react-core equivalent — use a custom styled element */}const MyComponent = () => ( <span style={{ display: "inline-block", width: "8px", height: "8px", borderRadius: "50%", backgroundColor: "green", }} aria-label="success" />);LocalNavigation
Change Type: Deprecated Action Required: Compose navigation from available react-core components
This component is deprecated as we work through new navigation patterns. If required, this component should be able to be composed from components in react-core.
Before
import { LocalNavigation } from "@cfa/react-components";
const MyComponent = () => ( <LocalNavigation> <LocalNavigation.Item href="/home">Home</LocalNavigation.Item> <LocalNavigation.Item href="/about">About</LocalNavigation.Item> <LocalNavigation.Item href="/contact">Contact</LocalNavigation.Item> </LocalNavigation>);After
import { Link } from "@cfa/react-core";
const MyComponent = () => ( <nav style={{ display: "flex", gap: "16px" }}> <Link href="/home">Home</Link> <Link href="/about">About</Link> <Link href="/contact">Contact</Link> </nav>);IconButtonGroup
Change Type: Deprecated
Action Required: Use multiple IconButton components with flex/grid layout
The IconButtonGroup has been deprecated in favor of using multiple IconButton components in a flex/grid layout.
Before
import { IconButtonGroup, IconButton } from "@cfa/react-components";import { Edit, Delete, Share } from "@cfa/system-icons";
const MyComponent = () => ( <IconButtonGroup> <IconButton icon={Edit} /> <IconButton icon={Delete} /> <IconButton icon={Share} /> </IconButtonGroup>);After
import { IconButton } from "@cfa/react-core";import { Edit, Delete, Share } from "@cfa/system-icons";
const MyComponent = () => ( <div style={{ display: "flex", gap: "4px" }}> <IconButton> <Edit /> </IconButton> <IconButton> <Delete /> </IconButton> <IconButton> <Share /> </IconButton> </div>);useMediaQuery
Change Type: Deprecated Action Required: Use CSS media queries or browser native APIs
This hook has been deprecated.
useBreakpoints
Change Type: Deprecated Action Required: Use CSS media queries or custom breakpoint solution
This hook has been deprecated.
CSS Considerations
React-core uses CSS modules with distinct class naming conventions (e.g., [class^='Label_label'], [class^='Input_input'], [class^='FieldError_fieldError']). The default styles differ from react-components in a number of ways that will likely require global CSS overrides in your application:
- Label styling — weights, sizes, and spacing differ
- Input styling — border radius, padding, and focus states may not match
- Checkbox and radio — visual appearance is different by default
- Disabled state — background colors differ (e.g.,
#EBEBEBvs#f7f8f8) - Error state — error field backgrounds and icon treatments differ
- Color tokens — some gray values differ between libraries (e.g.,
colorGray_7vscolorGray_8) - “Optional” label pattern — if your app renders an “(optional)” indicator on non-required fields, you’ll need to rebuild this behavior
Plan to audit and add global SCSS overrides alongside your component migrations, not after.
Common Gotchas
onChange handler signatures have changed
React-core onChange handlers return values directly, not DOM events. Audit every handler:
Checkbox:onChange(isSelected: boolean)— no moreevent.target.checkedRadioGroup:onChange(value: string)— no moreevent.target.valueSelect: usesonClickon individualSelect.Itemcomponents rather than a top-levelonChangeTextInput:onChange(value: string)— no moreevent.target.value
Styled-component wrappers need review
If you have styled(SomeReactComponentsComponent) wrappers, those will need updates. The internal DOM structure and CSS class names are different in react-core, so CSS selectors inside your styled-components may silently break. The shouldForwardProp pattern may also need adjustments, since react-core components may not accept the same custom props as their react-components equivalents.