Mantle Segmented Control Migration Guide
@ngrok/mantle has a new SegmentedControl component, and RadioGroup.ButtonGroup and RadioGroup.Button are gone. SegmentedControl.Root replaces RadioGroup.ButtonGroup, and SegmentedControl.Item replaces RadioGroup.Button. The props, the keyboard, the ARIA roles, and the form behavior are the same: both components share one radio primitive. The styles change: a thumb slides on a track in place of a row of bordered buttons. This guide moves a codebase to the new import.
TL;DR: Import SegmentedControl from @ngrok/mantle/segmented-control. Rename RadioGroup.ButtonGroup to SegmentedControl.Root and RadioGroup.Button to SegmentedControl.Item, in JSX and in typeof type queries. Delete a RadioGroup import that nothing reads any more. Change a data-slot selector from radio-group-button-group to segmented-control, and from radio-group-button to segmented-control-item.
Codemod
From a mantle checkout:
In a project that installs @ngrok/mantle:
The rules apply steps 2 and 3. They rename each RadioGroup.ButtonGroup and RadioGroup.Button JSX tag and typeof type query, in .tsx and .ts files. They add the SegmentedControl import after the first @ngrok/mantle/radio-group import of each file they change. The rules read a part through its import from @ngrok/mantle/radio-group, under its own name or an alias. A part that reaches the file through a local re-export or a destructured namespace stays manual. A function that binds RadioGroup to a parameter or a local shadows the import, so the rules leave that function alone. Steps 4 and 5 stay manual: whether a file still reads RadioGroup depends on the rest of the file, and a data-slot selector is a string, not syntax. The codemod is safe to run more than once. Pass --dry-run to print the rewrites first, and run your formatter after the rewrite.
You are an agent migrating a codebase. Do this:
- Find every
RadioGroup.ButtonGroupandRadioGroup.Button(parts of@ngrok/mantle'sRadioGroupcompound component). Search for both names, and for a renamed import of the same component (import { RadioGroup as MantleRadioGroup }) or a destructured one (const { ButtonGroup } = RadioGroup;). - In each file with a hit, add
import { SegmentedControl } from "@ngrok/mantle/segmented-control";. - Rename
RadioGroup.ButtonGrouptoSegmentedControl.RootandRadioGroup.ButtontoSegmentedControl.Item. Rename the opening tag, the closing tag, and eachtypeof RadioGroup.ButtonGrouportypeof RadioGroup.Buttonin a type. Keep every prop as it is. - When the file no longer reads
RadioGroup(noRadioGroup.Root,Item,List,ListItem,Card,Indicator,ItemContent, orInputSandbox), delete theRadioGroupimport. - Find a
data-slotselector in CSS, in a Tailwind variant, or in a test. Changeradio-group-button-grouptosegmented-controlandradio-group-buttontosegmented-control-item. Leaveradio-group,radio-group-item, and the otherradio-group-*slots alone. - After editing, run the project's formatter, linter, type checker, and tests.
Why a separate component? A segmented control switches a view or a mode in one row, and the change usually takes effect at once. A radio group is a form field whose options often carry a description. People and agents search for "segmented control", so the name now matches what they look for, in the import path, the component manifest, and the docs navigation. Underneath, it is still a radio group with the same contract.
Before → after
A segmented control
A file that also renders radios
A type query and an aliased import
A data-slot selector
Edge cases & gotchas
- A
RadioGroupimport that nothing reads. The codemod keeps it, because the file can still readRadioGroupelsewhere. Your linter flags it as unused; delete it. - Classes on the old parts.
SegmentedControl.Rootis a 36px (h-9) track with 4px (p-1) padding, so each item is about 28px tall. The thumb marks the checked item. Review eachh-*, border, and background class on aRadioGroup.ButtonGroupor aRadioGroup.Button: it can overflow the track or cover the thumb. - A type-only
SegmentedControlimport. The codemod keepsimport type { SegmentedControl }and adds no import, so the renamed JSX reads a type-only name. Change it to a value import. - A local name
SegmentedControl. The codemod adds an import under that name. When the file already declaresSegmentedControl, import the component under an alias and rename the tags to match. - A
SegmentedControl.ItemoutsideSegmentedControl.Root. It throws an error that names the root.RadioGroup.ButtoninsideRadioGroup.Rootrendered before; wrap the items inSegmentedControl.Rootin place ofRadioGroup.Root. - Tests. The roles are the same, so
getByRole("radiogroup")andgetByRole("radio", { name })still match. Only adata-slotquery changes.
How to verify
- Type check. A remaining
RadioGroup.ButtonGrouporRadioGroup.Buttonfails, because the parts are gone. - Search for
RadioGroup.ButtonGroup,RadioGroup.Button, andradio-group-button. Zero hits outside comments. - Load a page with a segmented control. The thumb sits under the checked segment, and a click, an arrow key, or a drag moves it.
- Run the project's formatter, linter, type checker, and tests.