Minor Changes
-
BREAKING CHANGE: remove legacy host-element
onprop support in@remix-run/component.Use the
on()mixin instead:- Old:
<button on={{ click() {} }} /> - New:
<button mix={[on('click', () => {})]} />
This change removes built-in host
onhandling from runtime, typing, and host-prop composition. Component-levelhandle.on(...)remains supported. - Old:
-
BREAKING CHANGE: remove legacy host-element
cssprop runtime support in@remix-run/component.Use the
css(...)mixin instead:- Old:
<div css={{ color: 'red' }} /> - New:
<div mix={[css({ color: 'red' })]} />
This aligns styling behavior with the new mixin composition model.
- Old:
-
BREAKING CHANGE: remove legacy host-element
animateprop runtime support in@remix-run/component.Use animation mixins instead:
- Old:
<div animate={{ enter: true, exit: true, layout: true }} /> - New:
<div mix={[animateEntrance(), animateExit(), animateLayout()]} />
This aligns animation behavior with the new mixin composition model.
- Old:
-
BREAKING CHANGE: remove legacy host-element
connectprop support in@remix-run/component.Use the
ref(...)mixin instead:- Old:
<div connect={(node, signal) => {}} /> - New:
<div mix={[ref((node, signal) => {})]} />
This aligns element reference and teardown behavior with the mixin composition model.
- Old:
-
BREAKING CHANGE: the
@remix-run/interactionpackage has been removed.handle.on(...)APIs were also removed from component and mixin handles.Before/after migration:
Interaction package APIs:
- Before:
defineInteraction(...),createContainer(...),on(target, listeners)from@remix-run/interaction. - After: use component APIs (
createMixin(...),on(...),addEventListeners(...)) from@remix-run/component.
// Before import { on } from '@remix-run/interaction' let dispose = on(window, { resize() { console.log('resized') }, }) // After import { addEventListeners } from '@remix-run/component' let controller = new AbortController() addEventListeners(window, controller.signal, { resize() { console.log('resized') }, })
Component handle API:
- Before:
handle.on(target, listeners). - After:
addEventListeners(target, handle.signal, listeners).
// Before function KeyboardTracker(handle: Handle) { handle.on(document, { keydown(event) { console.log(event.key) }, }) return () => null } // After import { addEventListeners } from '@remix-run/component' function KeyboardTracker(handle: Handle) { addEventListeners(document, handle.signal, { keydown(event) { console.log(event.key) }, }) return () => null }
Custom interaction patterns:
- Before:
defineInteraction(...)+ interaction setup function. - After: event mixins (
createMixin(...)) that composeon(...)listeners and dispatch typed custom events.
// Before import { defineInteraction, type Interaction } from '@remix-run/interaction' export let tempo = defineInteraction('my:tempo', Tempo) function Tempo(handle: Interaction) { handle.on(handle.target, { click() { handle.target.dispatchEvent(new TempoEvent(bmp)) }, }) } // App consumption (before, JSX) function TempoButtonBefore() { return () => ( <button on={{ [tempo](event) { console.log(event.bpm) }, }} /> ) } // After import { createMixin, on } from '@remix-run/component' export let tempo = 'my:tempo' as const export let tempoEvents = createMixin<HTMLElement>((handle) => { return () => ( <handle.element mix={[ on('click', (event) => { event.currentTarget.dispatchEvent(new TempoEvent(bpm)) }), ]} /> ) }) // App consumption (after) function TempoButton() { return () => ( <button mix={[ tempoEvents(), on(tempo, (event) => { console.log(event.detail.bpm) }), ]} /> ) }
TypedEventTarget
TypedEventTargetis now exported from@remix-run/component. - Before:
-
BREAKING CHANGE:
renderToStream(), hydration, client updates, and frame reloads no longer hoist baretitle,meta,link,style, orscript[type="application/ld+json"]elements intodocument.head. Render head content inside an explicit<head>instead, or pass values liketitleto a layout component that renders the head.This removes ordering-sensitive head manipulation from server rendering and client reconciliation. We originally explored this behavior in the spirit of React's head "float" work, but Remix Component's async model is centered on routes and frames rather than async components, so layouts can render head content explicitly without needing to discover and reorder tags from deep in the tree.
-
Add the new host
mixprop and mixin authoring APIs in@remix-run/component.New exports include:
createMixinMixinDescriptor,MixinHandle,MixinType,MixValueon(...)ref(...)css(...)
This enables reusable host behaviors and composable element capabilities without bespoke host props.
-
Add new interaction mixins for normalized user input events:
pressEvents(...)for pointer/keyboard "press" interactionskeysEvents(...)for keyboard key state events
These helpers provide a consistent mixin-based interaction model for input handling.
-
Add mixin-first animation APIs for host elements:
animateEntrance(...)animateExit(...)animateLayout(...)
These APIs move entrance/exit/layout animation behavior to composable mixins that can be combined with other host behaviors.
-
Allow the
mixprop to accept either a single mixin descriptor or an array of mixin descriptors.This lets one-off mixins use
mix={...}while preserving array support for composed mixins, and component render props now normalizemixto an array orundefinedso wrapper components can composemixvalues without special casing single descriptors. -
Allow client
resolveFrame(...)callbacks to returnRemixNodecontent in addition to HTML strings and streams.This lets apps render local frame fallback and recovery UI directly from the client runtime without manually serializing HTML, and frame updates now clear previously rendered HTML before mounting the new node-based content.
-
Automatically intercept anchor and area navigations through the Navigation API, with
rmx-targetto target mounted frames,rmx-srcto override the fetched frame source, andrmx-documentto opt back into full-document navigation. -
Add imperative frame-navigation runtime APIs and a
link(href, { src, target, history })mixin for declarative client navigations.run()now initializes fromrun({ loadModule, resolveFrame }), the package exportsnavigate(href, { src, target, history })andlink(href, { src, target, history }), and components can target mounted frames viahandle.frames.topandhandle.frames.get(name). Thelink()mixin addshref/rmx-*attributes to anchors and gives buttons and other elements accessible link semantics with click and keyboard navigation behavior. -
Allow
resolveFrame(src, signal, target)to receive the named frame target for targeted reloads.This makes it easier to distinguish targeted frame navigations when forwarding frame requests through app-specific fetch logic.
-
Add SSR frame source context for nested frame rendering.
renderToStream()now acceptsframeSrcandtopFrameSrc,resolveFrame()receives aResolveFrameContext, and server-rendered components can read stablehandle.frame.srcandhandle.frames.top.srcvalues across nested frame renders.
Patch Changes
-
Preserve browser-managed live state when frame DOM diffing updates interactive elements.
This keeps reloads from clobbering current UI state for reflected and form-like cases such as
details[open],dialog[open],input.checked, editable input values,textareavalues,<select>selection, and open popovers when the incoming HTML only changes serialized defaults. -
Forward hydrated client entry, frame reload, and
ready()initialization errors to the top-level runtime target returned byrun(), and type that runtime as aTypedEventTargetwith anerrorevent whose.errorvalue isunknown.This lets
app.addEventListener('error', ...)observe bubbling DOM errors captured by hydrated client entry roots, frame reload failures such as rejectedresolveFrame()calls, and initialization failures that rejectapp.ready(), while also giving TypeScript-aware consumers the concrete event names and safer payload types exposed byrun()and root listeners. -
Run mixin
insert,remove, andreclaimedlifecycle events in the scheduler's commit phase instead of dispatching them inline during DOM diffing.This lets
ref(...)and other insert-driven mixins safely callhandle.update()during initial mount, and it makes mixin lifecycle timing line up with commit-phase DOM state before normal queued tasks run. -
Fix full-document client reloads that could leave orphaned hydration markers behind when adjacent client entries are diffed in the same parent.
This prevents later navigations from failing with
Error: End marker not foundafter the live DOM ends up with mismatchedrmx:hstart and end markers. -
Fix SVG
classNameprop normalization to render asclassin both client DOM updates and SSR stream output.Also add SVG regression coverage to prevent accidental
class-nameoutput. -
Resolve nested SVG click targets back to their enclosing anchor or area element so frame navigation still intercepts normal link clicks inside inline SVG content.
-
Skip frame-navigation interception for native anchor and area elements with a
downloadattribute so browsers can handle file downloads normally without needingrmx-document.