Skip to content

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.

bash
npm install remix

Or install the standalone package:

bash
npm install @remix-run/component

Imports

Main Entry

ts
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

ts
import { renderToString, renderToStream } from 'remix/component/server'

JSX Runtime

Configured automatically via tsconfig.json. Manual import when needed:

ts
import { createElement } from 'remix/component/jsx-runtime'

TypeScript Configuration

json
{
  "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).

tsx
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:

tsx
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.

ts
handle.update()

handle.queueTask(task)

Schedules a task to run after the next update. The task receives an AbortSignal.

ts
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:

ts
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:

tsx
<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.

tsx
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.

tsx
import { Frame } from 'remix/component'

<Frame src="/api/books/details" name="details" fallback={<p>Loading...</p>} />

Props

PropTypeDescription
srcstringSource URL used when the frame loads or reloads its content.
namestringOptional frame name used for targeted navigation and lookups.
fallbackRenderableFallback content to render while the frame is pending.
onRecord<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/MethodTypeDescription
srcstringThe 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).

tsx
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.

tsx
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

OptionTypeDescription
frameSrcstring | URLSource URL to associate with the current frame render.
topFrameSrcstring | URLSource URL for the top-level frame in nested frame renders.
onError(error: unknown) => voidError hook invoked when rendering throws.
resolveFrame(src, target?, context?) => Promise<string | ReadableStream> | string | ReadableStreamCallback 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.

ts
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

PropertyTypeDescription
loadModule(moduleUrl: string, exportName: string) => Promise<Function> | FunctionLoads a module for client entry hydration.
resolveFrameResolveFrameOptional callback for resolving frame content during client navigation.

AppRuntime

The object returned by run():

MethodDescription
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.

ts
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.

ts
import { createRangeRoot } from 'remix/component'

let root = createRangeRoot([startNode, endNode])
root.render(<Widget />)

VirtualRoot

Both createRoot and createRangeRoot return a VirtualRoot:

MethodDescription
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.

ts
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.

tsx
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:

tsx
<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.

tsx
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'.

Enables client-side navigation without a full page reload.

tsx
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.

tsx
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.

tsx
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.

tsx
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.

tsx
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

PropertyTypeDefaultDescription
durationnumber150Animation duration in milliseconds.
easingstring'ease-out'CSS easing function.
delaynumber0Delay before animation starts.
compositeCompositeOperation---Web Animations API composite mode.
initialbooleantrueWhether to animate the first insertion. Set to false to skip the initial mount.
CSS propertiesstring | 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.

tsx
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.

tsx
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

PropertyTypeDefaultDescription
durationnumber200Animation duration in milliseconds.
easingstring'ease-out'CSS easing function.

Combining Mixins

Pass an array to the mix attribute to combine multiple mixins:

tsx
<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.

ts
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

PresetDurationBounceDescription
'smooth'400ms-0.3Overdamped, no oscillation
'snappy'200ms0Critically damped, fast approach
'bouncy'400ms0.3Underdamped, slight overshoot

SpringOptions

OptionTypeDefaultDescription
durationnumber300Perceptual duration in milliseconds (affects stiffness).
bouncenumber0Spring bounce from -1 (overdamped) to 0.95 (very bouncy). 0 = critically damped.
velocitynumber0Initial velocity in units per second.

SpringIterator

The returned iterator has these properties:

PropertyTypeDescription
durationnumberTime when the spring settles to rest (milliseconds).
easingstringCSS linear() easing function.
toString()stringReturns "<duration>ms <easing>" for CSS transitions.

spring.transition(property, presetOrOptions?, overrides?)

Helper that builds a CSS transition string for one or more properties:

ts
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.

ts
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

OptionTypeDescription
fromnumberStarting value.
tonumberEnding value.
durationnumberTotal duration in milliseconds.
curveBezierCurveCubic bezier curve for interpolation.

easings

Predefined cubic-bezier curves:

EasingControl 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.

tsx
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

MethodDescription
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.

Performs client-side navigation using the Navigation API. Integrates with the frame system.

ts
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)
})
OptionTypeDefaultDescription
srcstringhrefSource URL for the frame to load.
targetstring---Name of the frame to navigate. Defaults to the top frame.
history'push' | 'replace''push'Whether to push or replace the history entry.
resetScrollbooleantrueWhether to scroll to the top after navigation.

Types

TypeDescription
RemixNodeAnything renderable: string, number, JSX element, array, null, or undefined.
RemixElementA JSX element produced by createElement.
Handle<C>The component handle with context type C.
Context<C>The context storage API on handles.
FrameHandlePublic API for interacting with a frame instance.
FramePropsProps accepted by the built-in Frame component.
SpringIteratorIterator returned by spring().
SpringOptionsConfiguration for spring().
SpringPreset'smooth' | 'snappy' | 'bouncy'
TweenOptionsConfiguration for tween().
BezierCurveCubic bezier control points { x1, y1, x2, y2 }.
NavigationOptionsOptions for navigate().
VirtualRootRoot controller returned by createRoot and createRangeRoot.
AppRuntimeClient runtime returned by run().
RunInitOptions for run().
MixinDescriptorType returned by mixin functions for use with the mix attribute.
RefCallback<T>(node: T, signal: AbortSignal) => void
PressEventEvent type used by pressEvents.
EntryComponentA component marked as a client entry for hydration.

Released under the MIT License.