Remix
Remix

remix/ui/jsx-dev-runtime

Runtime UI primitives for Remix apps, including the component runtime, server rendering, frame hydration, reusable mixins, first-party components, and theme tokens.

Features

  • Component runtime APIs for rendering, hydration, frame navigation, and JSX
  • Server rendering APIs for streaming Remix UI trees and frames
  • mix composition with event, ref, CSS, and animation helpers
  • First-party components such as buttons, menus, listboxes, popovers, and selects
  • Fixed typed theme contract whose leaves resolve to var(--rmx-...)
  • createTheme() and createGlyphSheet() utilities for shared app styling and glyphs

Installation

npm i remix

Usage

Define your app theme once:

import { createTheme } from 'remix/ui'

let Theme = createTheme({
  space: {
    none: '0px',
    px: '1px',
    xs: '2px',
    sm: '4px',
    md: '8px',
    lg: '12px',
    xl: '16px',
    xxl: '24px',
  },
  radius: {
    none: '0px',
    sm: '4px',
    md: '8px',
    lg: '12px',
    xl: '16px',
    full: '9999px',
  },
  fontSize: {
    xxxs: '10px',
    xxs: '11px',
    xs: '12px',
    sm: '14px',
    md: '16px',
    lg: '18px',
    xl: '20px',
    xxl: '28px',
  },
  lineHeight: {
    tight: '1.2',
    normal: '1.5',
    relaxed: '1.7',
  },
  fontWeight: {
    normal: '400',
    medium: '500',
    semibold: '600',
    bold: '700',
  },
  shadow: {
    xs: '0 1px 2px rgb(0 0 0 / 0.05)',
    sm: '0 1px 3px rgb(0 0 0 / 0.10)',
    md: '0 4px 10px rgb(0 0 0 / 0.12)',
    lg: '0 10px 30px rgb(0 0 0 / 0.16)',
    xl: '0 20px 50px rgb(0 0 0 / 0.20)',
  },
  zIndex: {
    dropdown: '1000',
    popover: '1100',
    sticky: '1200',
    overlay: '1300',
    modal: '1400',
    toast: '1500',
    tooltip: '1600',
  },
  surface: {
    lvl0: '#ffffff',
    lvl1: '#f8fafc',
    lvl2: '#f1f5f9',
    lvl3: '#e5edf7',
    lvl4: '#dbe6f4',
  },
  colors: {
    text: {
      primary: '#111827',
      secondary: '#374151',
      muted: '#6b7280',
      link: '#2563eb',
    },
    border: {
      subtle: '#e5e7eb',
      default: '#d1d5db',
      strong: '#9ca3af',
    },
    focus: {
      ring: '#3b82f6',
    },
    overlay: {
      scrim: 'rgb(0 0 0 / 0.45)',
    },
    action: {
      primary: {
        background: '#2563eb',
        backgroundHover: '#1d4ed8',
        backgroundActive: '#1e40af',
        foreground: '#ffffff',
        border: '#2563eb',
      },
      secondary: {
        background: '#ffffff',
        backgroundHover: '#f8fafc',
        backgroundActive: '#f1f5f9',
        foreground: '#111827',
        border: '#d1d5db',
      },
      danger: {
        background: '#dc2626',
        backgroundHover: '#b91c1c',
        backgroundActive: '#991b1b',
        foreground: '#ffffff',
        border: '#dc2626',
      },
    },
  },
})

Render the theme once near the top of your document:

import type { Handle, RemixNode } from 'remix/ui'

function Layout(handle: Handle<{ children: RemixNode }>) {
  return () => (
    <html>
      <head>
        <Theme />
      </head>
      <body>{handle.props.children}</body>
    </html>
  )
}

Consume the shared token contract from app code and first-party components:

import { css } from 'remix/ui'
import { theme } from 'remix/ui'

let card = css({
  backgroundColor: theme.surface.lvl0,
  color: theme.colors.text.primary,
  border: `1px solid ${theme.colors.border.subtle}`,
  borderRadius: theme.radius.md,
  paddingInline: theme.space.md,
  paddingBlock: theme.space.sm,
})

<div mix={card} />

Render shared glyphs separately from the theme styles:

import type { Handle, RemixNode } from 'remix/ui'
import { Button } from 'remix/ui/button'
import { Glyph } from 'remix/ui/glyph'
import { RMX_01, RMX_01_GLYPHS } from 'remix/ui/theme'

function Layout(handle: Handle<{ children: RemixNode }>) {
  return () => (
    <html>
      <head>
        <RMX_01 />
      </head>
      <body>
        <RMX_01_GLYPHS />
        <Button startIcon={<Glyph name="add" />} tone="primary">
          New project
        </Button>
        {handle.props.children}
      </body>
    </html>
  )
}

Cascade Layers

Remix UI emits its built-in theme reset in rmx-reset and generated css(...) rules under rmx. Unlayered CSS outranks layered component CSS, so use explicit layer order when mixing Remix UI with global styles.

Put layers that should lose to Remix UI before rmx-reset and rmx:

@layer base, rmx-reset, rmx;

@layer base {
  button,
  input,
  textarea,
  select {
    font: inherit;
    margin: 0;
    padding: 0;
  }
}

License

See LICENSE