This is the changelog for ui. It follows semantic versioning.
-
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 })]} />
-
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 module -
Adopt 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.
-
BREAKING CHANGE: Remix UI component render functions no longer receive props as an argument. Type component props on
Handle<Props>and read current values fromhandle.propsin both setup and render code. -
Updated
anchor(floating, anchorTarget, options)to accept either anHTMLElementor coordinate target via the newAnchorPoint/AnchorTargettypes. -
Added
menu.contextTrigger()so menus can open from right-click pointer locations while keeping existing keyboard navigation, submenus, and selection behavior.
-
Fixed
css(...)so nested selector objects render recursively instead of serializing deeper nested rules as[object Object](see #11459). -
Dispatch reload events for nested frames when an ancestor frame reloads
-
Prevent non-blocking frames from displaying their fallback when an ancestor frame is reloaded
- Add a
signaloption torenderToStream()so request aborts can cancel pending frame rendering without invokingonError(see #11431).
-
Add explicit public API types for UI component, mixin, scheduler, stylesheet, animation, and theme helpers so generated declarations no longer depend on broad inference across helper factories (see #11433).
-
Fix rendering and JSX types for booleanish string attributes so
contentEditable={false},draggable={false},spellCheck={false}, and matching SVG attributes produce explicit"false"values instead of being omitted. ThetranslateJSX type now accepts the HTML attribute values"yes"and"no"(see #11434). -
Fix hydrated
@remix-run/uicomponents so non-rendering children inside fragments keep the correct DOM anchor when they later become renderable (see #11425). -
Ignore component updates scheduled after a frame reload has already removed that component, avoiding
Node.insertBeforeerrors from stale updates after the frame renders replacement markup (see #11422).
-
Fix a bug in Safari where cross-origin links to a new subdomain incorrectly set
event.canIntercept=trueand try to opt-into a<Frame>navigation which fails. Cross-origin links now correctly fall through to a document navigation in Safari. -
Keep streamed frame content in its template when a resolved frame stream starts with a doctype-only chunk.
-
Emit the built-in theme reset in
rmx-resetso generated Remix UI component styles can override it. Document where app layers should sit relative to Remix UI layers. -
Fixed layout animation interruptions so they restart from their current position and don't restart for updates that don't change their final position.
-
Improved type inference for
onmixinWhen defining a wrapper for
on, usetargetgeneric on your handler type:import { on, type Dispatched } from '@remix-run/ui' const ACCORDION_CHANGE_EVENT = 'rmx:accordion-change' as const type AccordionChangeEvent = Event & { accordionType: 'single' | 'multiple' itemValue: string value: string | null | string[] } declare global { interface HTMLElementEventMap { [ACCORDION_CHANGE_EVENT]: AccordionChangeEvent } } type AccordionChangeHandler<target extends HTMLElement> = ( event: Dispatched<AccordionChangeEvent, target>, signal: AbortSignal, ) => void | Promise<void> export function onAccordionChange<target extends HTMLElement>( handler: AccordionChangeHandler<target>, captureBoolean?: boolean, ) { return on(ACCORDION_CHANGE_EVENT, handler, captureBoolean) } let button = ( <button mix={[ onAccordionChange((event, signal) => { event // ^? Dispatched<AccordionChangeEvent, HTMLButtonElement> event.currentTarget // ^? HTMLButtonElement }), ]} /> )
-
Preserve hydrated client entry instances and nested frame resolution during full-document root frame reloads.
-
Document the
run()loadModuleandresolveFramehooks so editor hints explain how to hydrate client entries and resolve browser-loaded frames. -
Optimize UI runtime hot paths.
- Fast path for plain
on()mixins that patches host listeners in place. - Lazy direct listener closures for event listeners managed by the runtime.
- Lazy mixin scope signals to avoid unnecessary AbortController work.
- Faster keyed reconciliation for in-order, append-only, single-removal, and pair-swap lists.
- Property-level patching for object styles during updates.
- Bulk clearing for removable child lists, with an innerHTML guard.
- Fast path for plain
-
Fix a flash of unstyled content when navigating between two pages whose hydrated client entries use different
css()rules. Style adoption now releases prior-page server styles by refcount instead of resetting the adopted stylesheet, so DOM preserved across a reload (e.g. inside a still-hydrated client-entry boundary) keeps its rules until the new module finishes loading and replaces it. -
Fix server rendering for
<textarea value>,<textarea defaultValue>,<input defaultValue>, and<input defaultChecked>so initial form control content matches client rendering, and disallow textarea children in JSX types.
-
Improved runtime rendering performance by reducing child normalization, keyed reconciliation, mixin lifecycle, scheduler phase, and host insertion overhead.
-
Stripped
<!DOCTYPE>markup from server and client frame responses before rendering frame content.
-
BREAKING CHANGE: Consolidated the deprecated
@remix-run/componentpackage into@remix-run/ui. Import component runtime APIs from@remix-run/ui, server rendering APIs from@remix-run/ui/server, JSX runtime APIs from@remix-run/ui/jsx-runtimeand@remix-run/ui/jsx-dev-runtime, and animation APIs from@remix-run/ui/animation.Removed the deprecated
@remix-run/ui/on-outside-pointer-downexport. Use the popover, menu, or other component-level outside interaction APIs instead. -
BREAKING CHANGE: Components now receive props through a stable
handle.propsobject usingHandle<Props, Context>instead of receiving a separatesetupargument and render callback props. Move initialization values that previously used<Component setup={...} />onto regular props, and read all props fromhandle.propsin both the component function and render callback.Before:
function Counter(handle: Handle<CounterContext>, setup: { initialCount: number }) { let count = setup.initialCount return (props: { label: string }) => ( <button> {props.label}: {count} </button> ) } ;<Counter setup={{ initialCount: 10 }} label="Count" />
After:
function Counter(handle: Handle<{ initialCount: number; label: string }, CounterContext>) { let count = handle.props.initialCount return () => ( <button> {handle.props.label}: {count} </button> ) } ;<Counter initialCount={10} label="Count" />
The
handle.propsobject keeps the same identity for the component lifetime while its values are updated before each render, so destructuringlet { props, update } = handleremains safe. Thesetupprop is no longer special and is treated like any other prop.This also removes the old pattern where setup-scope helpers had to read from a mutable variable that was reassigned inside the render callback:
function Listbox(handle: Handle<ListboxContext>) { let props: ListboxProps function select(value: string) { props.onSelect(value) } handle.context.set({ select }) return (nextProps: ListboxProps) => { props = nextProps return props.children } }
Helpers can now read the current props directly from the stable handle:
function Listbox(handle: Handle<ListboxProps, ListboxContext>) { function select(value: string) { handle.props.onSelect(value) } handle.context.set({ select }) return () => handle.props.children }
-
BREAKING CHANGE: Removed the deprecated
keysEvents,pressEvents, andPressEventexports from@remix-run/ui. Useon(...)with native DOM keyboard, pointer, and click events directly instead.