Release Notes
Minor Changes
BREAKING CHANGE: Replaced the styled button component API with a default
button()mixin exported from@remix-run/ui/button.Use the mixin directly on button-like hosts instead of importing
Buttonor composing the previous slot style exports:import button from '@remix-run/ui/button' <button mix={button()}>Edit order</button> <button mix={button({ size: 'lg', tone: 'primary' })}>Add product</button> <button mix={button({ tone: 'ghost' })}>Cancel</button>Added a default
checkbox()mixin exported from@remix-run/ui/checkboxfor styling native checkbox inputs.Checkbox controls use the same keyboard focus shadow as
input()controls and support an optional visualstatefor app-owned checked, unchecked, and mixed states.import checkbox from '@remix-run/ui/checkbox' <input defaultChecked mix={checkbox()} name="permissions" value="read" /> <input indeterminate mix={checkbox({ size: 'lg', state: 'mixed' })} />Added top-level component exports for headless primitives and styled components.
Primitive-only modules import directly from their component path, while modules with styled wrappers expose lower-level behavior under
/primitives:import button from '@remix-run/ui/button' import * as select from '@remix-run/ui/select/primitives'BREAKING CHANGE: Removed the
@remix-run/ui/components/*subpath exports. Import component modules from@remix-run/ui/*instead.BREAKING CHANGE: Removed root helper exports that were only intended for first-party component internals:
flashAttributehiddenTypeaheadmatchNextItemBySearchTextonKeyDownSearchValuewaitwaitForCssTransition
Removed the
@remix-run/ui/scroll-locksubpath export. Scroll locking is now an internal popover implementation detail.Added a default
input()mixin exported from@remix-run/ui/inputfor standalone native inputs, plusinput.root()andinput.field()for icon-capable input layouts.import input from '@remix-run/ui/input' <input mix={input()} placeholder="Limit" /> <div mix={input.root()}> <SearchIcon /> <input mix={input.field()} placeholder="Search and filter products" /> </div>Added a default
radio()mixin exported from@remix-run/ui/radiofor styling native radio inputs.Radio controls use the same keyboard focus shadow as
input()controls.import radio from '@remix-run/ui/radio' <input defaultChecked mix={radio()} name="shipping-speed" value="standard" /> <input mix={radio({ size: 'lg' })} name="shipping-speed" value="express" />Added styled component subpath exports under
@remix-run/ui/*for accordion, breadcrumbs, checkbox, combobox, menu, and select. These are the package-owned implementations behind theremix/ui/*entrypoints.Added
tabsandtabs/primitivesexports for controlled and uncontrolled tab groups with toggle-slider active tabs, button-sized tab text, active-tab panels, keyboard activation, and bubbling tab change events.import { Tabs, TabList, Tab, TabPanel } from '@remix-run/ui/tabs' ;<Tabs defaultActiveTab="overview"> <TabList aria-label="Project sections"> <Tab name="overview">Overview</Tab> <Tab name="activity">Activity</Tab> </TabList> <TabPanel name="overview">Project summary.</TabPanel> <TabPanel name="activity">Recent changes.</TabPanel> </Tabs>Added
toggle()styles andtoggle/primitivesfor boolean switch controls with medium and large sizes.import toggle from '@remix-run/ui/toggle' import * as togglePrimitive from '@remix-run/ui/toggle/primitives' <input defaultChecked mix={toggle({ size: 'lg' })} /> <button aria-label="Notifications" mix={[...toggle(), togglePrimitive.control({ defaultChecked: true })]} />
Patch Changes
Forward the frame's name as the resolve target when a named
<Frame>is resolved on the clientOnly the reload and server resolve paths passed the frame's name; the client resolve path — a fresh client mount, or a
clientEntry-wrapped frame remounted when a non-root ancestor reloads — calledresolveFramewithout it. Frames that branch on the target (for example via anX-Remix-Targetheader) now receive the correct content instead of the no-target response.Fixed hydration for multiple
clientEntrycomponents in the same moduleAdopt a Fragment-nested
<Frame>'s server-rendered hydration marker atclientEntryboundariesA
<Frame>that is the first child of a bare Fragment returned by aclientEntrynow adopts its streamed hydration marker instead of taking the fresh-insert path, which previously re-fetchedsrcon the client and duplicated the streamed subtree. A<Frame>wrapped in a host element already hydrated cleanly.