Skip to content

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-coreWhat ChangedNotes
DropdownSelectRename + API ChangeReplace all Dropdown imports/usages with Select
MultiselectSelectRename + API ChangeUse <Select.Root selectionMode="multiple">
SearchDropdownComboboxRename + API ChangeNew component with different state management approach
TextFieldTextInput / TextAreaInputRename + API ChangeMultiline fields use TextAreaInput; single-line use TextInput
RadioButtonRadioRename + API ChangeLabel is now passed as children, not a prop
RadioGroupRadioGroupAPI ChangeSame name; props changed: isDisabled, isRequired, isInvalid, errorMessage (not errorText)
CheckboxCheckboxAPI ChangeSame name; checkedisSelected, onChange(event)onChange(boolean)
DatePickerDatePickerStabilizedMoved from UNSTABLE_DatePicker to DatePicker (stable as of v1.0.2)
TimePickerNo direct replacementRebuild as a Select-based dropdown with a custom time options utility
InputLabelLabelRenameSimply renamed; or use the label prop built into most react-core form components
HelperTextText with slot="description"RemovedUse Text component with slot prop instead
IconDirect icon importsRemovedImport icons directly from respective icon packages
ListListBox / GridListSplitUse ListBox for static items, GridList for interactive elements
LogoDirect logo importsPackage ChangeImport from @cfa/brand-icons/logos
TagChip / TagGroupRename + API ChangeChip is display-only; use TagGroup with onRemove for dismissible tags
TypographyHeading + HTML elementsDeprecatedUse Heading for headlines, HTML elements for body text (<span>, <p>)
ButtonGroupDeprecatedUse custom layout with multiple buttons
FormGroupCheckboxGroup / RadioGroupReplacedUse dedicated group components
StatusIndicatorDeprecatedNo direct replacement; use a custom styled element until a dedicated component is available
LocalNavigationDeprecatedCan be composed with existing react-core components
IconButtonGroupMultiple IconButtonDeprecatedUse IconButton’s in flex/grid layout
useMediaQueryDeprecatedUse CSS media queries
useBreakpointsDeprecatedUse 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
disabledisDisabled
requiredisRequired
checked / value (boolean)isSelected
errorTextisInvalid + <Component.Error>
helperTextdescription or <Component.Description>
label (as prop)Children or <Component.Label>
onChange(event)onChange(value) — value directly, not event

Major Component Changes


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:

  • ListBox should be used for static text elements
  • GridList is 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>
);

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 only
const DisplayExample = () => <Chip>Display only</Chip>;
// Dismissible — use TagGroup with onRemove
const 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: 400
  • body2: font-size: 14px; font-weight: 400
  • caption1: font-size: 12px; font-weight: 500
  • caption2: 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 invalid
  • description — helper text shown when the field is valid (hidden automatically when isInvalid is 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., #EBEBEB vs #f7f8f8)
  • Error state — error field backgrounds and icon treatments differ
  • Color tokens — some gray values differ between libraries (e.g., colorGray_7 vs colorGray_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 more event.target.checked
  • RadioGroup: onChange(value: string) — no more event.target.value
  • Select: uses onClick on individual Select.Item components rather than a top-level onChange
  • TextInput: onChange(value: string) — no more event.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.