component
The component package is Remix's lightweight JSX component system. It supports server-side rendering to strings and streams, client-side hydration, a mixin system for behavior composition, and built-in animation primitives.
Installation
The component system is included with Remix. No additional installation is needed.
npm install remixOr install the standalone package:
npm install @remix-run/componentImports
Main Entry
import {
// Components
Fragment,
Frame,
// Mixins
css,
on,
ref,
link,
pressEvents,
keysEvents,
animateEntrance,
animateExit,
animateLayout,
// Animation
spring,
tween,
easings,
// Navigation
navigate,
// Roots
run,
createRoot,
createRangeRoot,
createScheduler,
// Client Entries
clientEntry,
// Utilities
createElement,
createMixin,
TypedEventTarget,
addEventListeners,
} from 'remix/component'Server Rendering
import { renderToString, renderToStream } from 'remix/component/server'JSX Runtime
Configured automatically via tsconfig.json. Manual import when needed:
import { createElement } from 'remix/component/jsx-runtime'TypeScript Configuration
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "remix/component"
}
}Component Model
A Remix component is a function that returns a render function. The outer function is the setup phase (runs once); the inner function is the render phase (runs each time props change).
function Greeting() {
return ({ name }: { name: string }) => (
<p>Hello, {name}!</p>
)
}With the Handle API
When a component needs interactivity, the setup function accepts a handle parameter and returns a function that receives the handle, which in turn returns the render function:
function Counter() {
return (handle) => {
let count = 0
return ({ initial = 0 }: { initial?: number }) => {
count = initial
return (
<div>
<p>Count: {count}</p>
<button mix={on('click', () => {
count++
handle.update()
})}>
Increment
</button>
</div>
)
}
}
}Handle API
The Handle object provides runtime capabilities to interactive components.
handle.update()
Schedules a re-render of the component. Returns a Promise<AbortSignal> that resolves after the update completes. The signal is aborted when the component re-renders again or is removed.
handle.update()handle.queueTask(task)
Schedules a task to run after the next update. The task receives an AbortSignal.
handle.queueTask(async (signal) => {
let response = await fetch('/api/data', { signal })
data = await response.json()
handle.update()
})handle.signal
An AbortSignal that is aborted when the component is disconnected from the tree. Use it for cleanup:
let interval = setInterval(() => handle.update(), 1000)
handle.signal.addEventListener('abort', () => clearInterval(interval))handle.id
A stable unique identifier for the component instance. Useful for generating DOM IDs:
<label for={`input-${handle.id}`}>Name</label>
<input id={`input-${handle.id}`} />handle.context
Access the component tree's context system. See Context API below.
handle.frame
The component's closest FrameHandle. Provides access to the current frame's src, reload(), and replace() methods.
handle.frames
Access named frames in the current runtime tree:
handle.frames.top--- The root frame for the current runtime tree.handle.frames.get(name)--- Look up a named frame by name.
Fragment
Groups children without adding an extra DOM element.
import { Fragment } from 'remix/component'
// Using the Fragment component
<Fragment>
<p>First</p>
<p>Second</p>
</Fragment>
// Using the shorthand syntax
<>
<p>First</p>
<p>Second</p>
</>Frame
The Frame component renders streaming server content into a page boundary. It can load content from a URL and replace a fallback placeholder.
import { Frame } from 'remix/component'
<Frame src="/api/books/details" name="details" fallback={<p>Loading...</p>} />Props
| Prop | Type | Description |
|---|---|---|
src | string | Source URL used when the frame loads or reloads its content. |
name | string | Optional frame name used for targeted navigation and lookups. |
fallback | Renderable | Fallback content to render while the frame is pending. |
on | Record<string, (event, signal) => void> | Event handlers invoked for events dispatched from the frame element. |
FrameHandle
The FrameHandle type provides the public API for interacting with a frame instance:
| Property/Method | Type | Description |
|---|---|---|
src | string | The current source URL. |
reload() | Promise<AbortSignal> | Reloads the frame content from its src. |
replace(content) | Promise<void> | Replaces the frame content directly. Accepts a ReadableStream, string, or RemixNode. |
The FrameHandle also extends TypedEventTarget and emits reloadStart and reloadComplete events.
Server Rendering
renderToString(element)
Renders a component tree to a complete HTML string. Suitable for small pages and cases where you need the full HTML as a single value (caching, email).
import { renderToString } from 'remix/component/server'
let html = renderToString(
<Layout title="Home">
<h1>Welcome</h1>
</Layout>,
)
return new Response(html, {
headers: { 'Content-Type': 'text/html; charset=UTF-8' },
})renderToStream(element, options?)
Renders a component tree progressively as a ReadableStream. The browser starts rendering HTML as chunks arrive, improving time-to-first-byte.
import { renderToStream } from 'remix/component/server'
let stream = renderToStream(
<Layout title="Home">
<h1>Welcome</h1>
</Layout>,
{
frameSrc: request.url,
onError: (error) => console.error(error),
},
)
return new Response(stream, {
headers: { 'Content-Type': 'text/html; charset=UTF-8' },
})RenderToStreamOptions
| Option | Type | Description |
|---|---|---|
frameSrc | string | URL | Source URL to associate with the current frame render. |
topFrameSrc | string | URL | Source URL for the top-level frame in nested frame renders. |
onError | (error: unknown) => void | Error hook invoked when rendering throws. |
resolveFrame | (src, target?, context?) => Promise<string | ReadableStream> | string | ReadableStream | Callback used to resolve nested frame content during streaming SSR. |
Client Hydration
run(init)
Starts the client-side Remix component runtime for the current document. Returns an AppRuntime object.
import { run } from 'remix/component'
let app = run({
loadModule: (url, name) => import(url).then(m => m[name]),
resolveFrame: async (src) => {
let response = await fetch(src)
return response.body
},
})
app.addEventListener('error', (event) => {
console.error('Component error:', event.error)
})
await app.ready()RunInit
| Property | Type | Description |
|---|---|---|
loadModule | (moduleUrl: string, exportName: string) => Promise<Function> | Function | Loads a module for client entry hydration. |
resolveFrame | ResolveFrame | Optional callback for resolving frame content during client navigation. |
AppRuntime
The object returned by run():
| Method | Description |
|---|---|
ready() | Returns a promise that resolves when the initial hydration completes. |
flush() | Synchronously processes all pending updates. |
dispose() | Tears down the runtime and removes all listeners. |
addEventListener('error', handler) | Listens for ComponentErrorEvent errors from the component tree. |
createRoot(container, options?)
Creates a virtual DOM root for client-side rendering into a container element.
import { createRoot } from 'remix/component'
let root = createRoot(document.getElementById('app'))
root.render(<App />)
// Later:
root.dispose()createRangeRoot(boundaries, options?)
Creates a virtual root bounded by two DOM nodes (typically comment markers). Used for rendering into specific regions of the page.
import { createRangeRoot } from 'remix/component'
let root = createRangeRoot([startNode, endNode])
root.render(<Widget />)VirtualRoot
Both createRoot and createRangeRoot return a VirtualRoot:
| Method | Description |
|---|---|
render(element) | Renders or updates the element tree within the root. |
dispose() | Removes all rendered content and cleans up. |
flush() | Synchronously processes all pending work. |
addEventListener('error', handler) | Listens for component errors within this root. |
clientEntry(href, component)
Marks a component as a client entry for hydration. The href is the module URL with optional export name.
import { clientEntry } from 'remix/component'
export let InteractiveWidget = clientEntry(
'/js/interactive-widget.js#InteractiveWidget',
function InteractiveWidget(handle) {
// ...
return (props) => <div>...</div>
},
)Mixins
Mixins are reusable units of behavior and styling attached to elements via the mix attribute.
css(styles)
Applies scoped CSS styles using generated class names. Properties use camelCase.
import { css } from 'remix/component'
<div mix={css({
padding: '1rem',
backgroundColor: '#f0f0f0',
fontSize: '14px',
})}>
Styled content
</div>Supports pseudo-selectors and media queries through nested objects:
<button mix={css({
background: 'blue',
color: 'white',
':hover': {
background: 'darkblue',
},
'@media (max-width: 768px)': {
padding: '0.5rem',
},
})}>
Responsive Button
</button>on(event, handler)
Attaches a DOM event listener to an element.
import { on } from 'remix/component'
<button mix={on('click', (event) => {
console.log('Clicked!', event.target)
})}>
Click Me
</button>Common events: 'click', 'input', 'change', 'submit', 'keydown', 'focus', 'blur', 'mouseenter', 'mouseleave'.
link(href)
Enables client-side navigation without a full page reload.
import { link } from 'remix/component'
<a mix={link('/books')}>Browse Books</a>ref(callback)
Calls a callback when an element is inserted into the DOM. The callback receives the DOM node and an AbortSignal that is aborted when the element is removed.
import { ref } from 'remix/component'
<canvas mix={ref((canvas, signal) => {
let ctx = canvas.getContext('2d')
// Draw on the canvas...
signal.addEventListener('abort', () => {
// Cleanup when element is removed
})
})} />pressEvents(options)
Provides normalized press handling across mouse, touch, and keyboard interactions.
import { pressEvents } from 'remix/component'
<button mix={pressEvents({
onPress: () => console.log('pressed'),
onPressStart: () => console.log('press start'),
onPressEnd: () => console.log('press end'),
})}>
Press Me
</button>keysEvents(bindings)
Handles keyboard shortcuts on an element.
import { keysEvents } from 'remix/component'
<div mix={keysEvents({
Escape: () => closeModal(),
Enter: () => submit(),
'Ctrl+S': () => save(),
})}>
Press Escape to close
</div>animateEntrance(config?)
Animates an element when it is inserted into the DOM. Pass true for defaults, false to disable, or a configuration object.
import { animateEntrance } from 'remix/component'
// Default fade-in (150ms ease-out, opacity 0 -> 1)
<div mix={animateEntrance()}>Appears with animation</div>
// Custom entrance
<div mix={animateEntrance({
opacity: 0,
transform: 'translateY(20px)',
duration: 300,
easing: 'ease-out',
})}>
Slides up
</div>AnimateMixinConfig
| Property | Type | Default | Description |
|---|---|---|---|
duration | number | 150 | Animation duration in milliseconds. |
easing | string | 'ease-out' | CSS easing function. |
delay | number | 0 | Delay before animation starts. |
composite | CompositeOperation | --- | Web Animations API composite mode. |
initial | boolean | true | Whether to animate the first insertion. Set to false to skip the initial mount. |
| CSS properties | string | number | --- | Any CSS property (e.g., opacity, transform) specifying the starting value. |
animateExit(config?)
Animates an element when it is removed from the DOM. The element is kept in the DOM until the animation finishes.
import { animateExit } from 'remix/component'
// Default fade-out (150ms ease-in, opacity 1 -> 0)
<div mix={animateExit()}>Disappears with animation</div>
// Custom exit
<div mix={animateExit({
opacity: 0,
transform: 'scale(0.9)',
duration: 200,
easing: 'ease-in',
})}>
Shrinks away
</div>animateLayout(config?)
Animates layout changes using FLIP-style transforms. When an element's position or size changes between renders, it smoothly transitions from the old layout to the new one.
import { animateLayout } from 'remix/component'
<div mix={animateLayout()}>
Moves smoothly when layout changes
</div>
// Custom duration and easing
<div mix={animateLayout({
duration: 300,
easing: 'ease-in-out',
})}>
Custom layout animation
</div>LayoutAnimationConfig
| Property | Type | Default | Description |
|---|---|---|---|
duration | number | 200 | Animation duration in milliseconds. |
easing | string | 'ease-out' | CSS easing function. |
Combining Mixins
Pass an array to the mix attribute to combine multiple mixins:
<button mix={[
css({ padding: '0.5rem 1rem', background: 'blue', color: 'white' }),
on('click', handleClick),
pressEvents({ onPress: handlePress }),
animateEntrance(),
animateExit(),
]}>
Interactive Button
</button>Animation
spring(presetOrOptions?, overrides?)
Creates a spring physics iterator for animations. Returns a SpringIterator with duration and easing properties.
import { spring } from 'remix/component'
// Using a preset
let s = spring('bouncy')
// Using custom options
let s = spring({ duration: 400, bounce: 0.3 })
// As a CSS transition string
element.style.transition = `transform ${spring('snappy')}`
// Result: "transform 200ms linear(...)"
// Spread for Web Animations API
element.animate(keyframes, { ...spring() })
// Iterate for frame-by-frame animation
for (let position of spring('bouncy')) {
element.style.transform = `translateX(${position * 100}px)`
}Presets
| Preset | Duration | Bounce | Description |
|---|---|---|---|
'smooth' | 400ms | -0.3 | Overdamped, no oscillation |
'snappy' | 200ms | 0 | Critically damped, fast approach |
'bouncy' | 400ms | 0.3 | Underdamped, slight overshoot |
SpringOptions
| Option | Type | Default | Description |
|---|---|---|---|
duration | number | 300 | Perceptual duration in milliseconds (affects stiffness). |
bounce | number | 0 | Spring bounce from -1 (overdamped) to 0.95 (very bouncy). 0 = critically damped. |
velocity | number | 0 | Initial velocity in units per second. |
SpringIterator
The returned iterator has these properties:
| Property | Type | Description |
|---|---|---|
duration | number | Time when the spring settles to rest (milliseconds). |
easing | string | CSS linear() easing function. |
toString() | string | Returns "<duration>ms <easing>" for CSS transitions. |
spring.transition(property, presetOrOptions?, overrides?)
Helper that builds a CSS transition string for one or more properties:
element.style.transition = spring.transition('transform', 'bouncy')
// "transform 400ms linear(...)"
element.style.transition = spring.transition(['transform', 'opacity'], 'snappy')
// "transform 200ms linear(...), opacity 200ms linear(...)"tween(options)
Generator that tweens a value over time using a cubic bezier curve. Yields the current value on each animation frame.
import { tween, easings } from 'remix/component'
let animation = tween({
from: 0,
to: 100,
duration: 300,
curve: easings.easeInOut,
})
// Drive the animation with timestamps
let result = animation.next() // { value: 0, done: false }
result = animation.next(performance.now()) // { value: 42.5, done: false }TweenOptions
| Option | Type | Description |
|---|---|---|
from | number | Starting value. |
to | number | Ending value. |
duration | number | Total duration in milliseconds. |
curve | BezierCurve | Cubic bezier curve for interpolation. |
easings
Predefined cubic-bezier curves:
| Easing | Control Points |
|---|---|
easings.linear | (0, 0, 1, 1) |
easings.ease | (0.25, 0.1, 0.25, 1) |
easings.easeIn | (0.42, 0, 1, 1) |
easings.easeOut | (0, 0, 0.58, 1) |
easings.easeInOut | (0.42, 0, 0.58, 1) |
Each easing is a BezierCurve object with x1, y1, x2, y2 properties.
Context API
The Context API allows passing data through the component tree without prop drilling.
function ThemeProvider(handle) {
handle.context.set({ theme: 'dark' })
return ({ children }) => <>{children}</>
}
function ThemedButton() {
return (handle) => {
let { theme } = handle.context.get(ThemeProvider)
return ({ label }: { label: string }) => (
<button class={`btn-${theme}`}>{label}</button>
)
}
}
// Usage
<ThemeProvider>
<ThemedButton label="Click Me" />
</ThemeProvider>Context Methods
| Method | Description |
|---|---|
handle.context.set(values) | Sets the context value for this component instance. Descendant components can read it. |
handle.context.get(component) | Reads the context value set by the nearest ancestor of the given component type. |
Navigation
navigate(href, options?)
Performs client-side navigation using the Navigation API. Integrates with the frame system.
import { navigate } from 'remix/component'
await navigate('/books/123')
// With options
await navigate('/books', {
target: 'content', // Navigate a named frame
src: '/api/books', // Custom source URL for the frame
history: 'replace', // 'push' (default) or 'replace'
resetScroll: true, // Reset scroll position (default: true)
})NavigationOptions
| Option | Type | Default | Description |
|---|---|---|---|
src | string | href | Source URL for the frame to load. |
target | string | --- | Name of the frame to navigate. Defaults to the top frame. |
history | 'push' | 'replace' | 'push' | Whether to push or replace the history entry. |
resetScroll | boolean | true | Whether to scroll to the top after navigation. |
Types
| Type | Description |
|---|---|
RemixNode | Anything renderable: string, number, JSX element, array, null, or undefined. |
RemixElement | A JSX element produced by createElement. |
Handle<C> | The component handle with context type C. |
Context<C> | The context storage API on handles. |
FrameHandle | Public API for interacting with a frame instance. |
FrameProps | Props accepted by the built-in Frame component. |
SpringIterator | Iterator returned by spring(). |
SpringOptions | Configuration for spring(). |
SpringPreset | 'smooth' | 'snappy' | 'bouncy' |
TweenOptions | Configuration for tween(). |
BezierCurve | Cubic bezier control points { x1, y1, x2, y2 }. |
NavigationOptions | Options for navigate(). |
VirtualRoot | Root controller returned by createRoot and createRangeRoot. |
AppRuntime | Client runtime returned by run(). |
RunInit | Options for run(). |
MixinDescriptor | Type returned by mixin functions for use with the mix attribute. |
RefCallback<T> | (node: T, signal: AbortSignal) => void |
PressEvent | Event type used by pressEvents. |
EntryComponent | A component marked as a client entry for hydration. |
Related
- Components & JSX --- Conceptual guide to the component model.
- Streaming --- Streaming HTML with
renderToStream. - Styling --- Styling patterns with the
cssmixin.