[email protected]
ui v0.4.0
create-remix
View on GitHubView PackagePublished: Jul 1, 2026

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 Button or 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/checkbox for styling native checkbox inputs.

    Checkbox controls use the same keyboard focus shadow as input() controls and support an optional visual state for 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:

    • flashAttribute
    • hiddenTypeahead
    • matchNextItemBySearchText
    • onKeyDown
    • SearchValue
    • wait
    • waitForCssTransition

    Removed the @remix-run/ui/scroll-lock subpath export. Scroll locking is now an internal popover implementation detail.

  • Added a default input() mixin exported from @remix-run/ui/input for standalone native inputs, plus input.root() and input.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/radio for 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 the remix/ui/* entrypoints.

  • Added tabs and tabs/primitives exports 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 and toggle/primitives for 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 client

    Only 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 — called resolveFrame without it. Frames that branch on the target (for example via an X-Remix-Target header) now receive the correct content instead of the no-target response.

  • Fixed hydration for multiple clientEntry components in the same module

  • Adopt a Fragment-nested <Frame>'s server-rendered hydration marker at clientEntry boundaries

    A <Frame> that is the first child of a bare Fragment returned by a clientEntry now adopts its streamed hydration marker instead of taking the fresh-insert path, which previously re-fetched src on the client and duplicated the streamed subtree. A <Frame> wrapped in a host element already hydrated cleanly.