Getting started

Cross-platform

Motif targets two runtimes from one source tree. Both branches export the same component names, accept the same props, and read from the same theme. The branch is invisible at the call site — the bundler picks one at install time, and your code never has to choose.

Updated 3 days agoEdit on GitHubWeb & native

One package, two builds

usemotif is the only package an application installs. It ships two compiled entry points:

  • dist/index.js — the web build. Wires @usemotif/react underneath.
  • dist/index.native.js — the native build. Wires @usemotif/react-native underneath.

The package's exports field declares a react-native condition. Metro and Expo see that condition and resolve the import to the native build; Vite, webpack, esbuild, and Rollup ignore it and pick up the default. There is no flag to set, no platform check to write — the bundler picks the right binary.

Both builds share the type declarations, so editor tooling does not split between platforms either.

The surface is identical

Open the import line on a web file and on a native file. They are the same:

example.tsx
import { Box, Stack, Text, ThemeProvider, createTheme, styled } from 'usemotif';

The component names, the prop shapes, the theme model, the responsive rules — all of it lines up. A <Box bg="$colors.surface.base" p="$space.4" _hover={...} /> on the web is the same component call as <Box bg="$colors.surface.base" p="$space.4" _hover={...} /> on native. You write the prop; the platform reads it.

That is the part that took the most work. A naive port would expose a different API on each side and let consumers switch with import { Box } from '@usemotif/react' on web and import { Box } from '@usemotif/react-native' on native. Motif chooses the harder path: the platform decides at the bundler level, and the application code never participates.

Two runtimes, one model

Underneath the shared surface, the platforms work differently.

On web, motif compiles tokens into a <style> element of CSS custom properties, scoped per [data-theme="<name>"]. Token references on style props become var(--…) strings; responsive objects become media queries injected into a stylesheet and shared across the page; pseudo-state bags become real :hover / :focus-visible / :active rules hashed into a class. Theme switches are attribute changes on <html>. The browser is doing the work.

On native, motif keeps the tokens in a context node. There is no cascade and no stylesheet, so token references resolve to literal values at render time, responsive values pick the right slot against the active dimensions, and pseudo-state bags drive Pressable state callbacks. Theme switches re-read the active branch of the tree.

The model — atoms, named compositions, an explicit merge order — is the same on either side. The output is what differs.

Where the platforms diverge

A few things only make sense on one runtime, and motif does not pretend otherwise.

  • SSR. SSRStyleCollector and CollectorContext only exist on the web build. They flush generated rules into the HTML stream so the document arrives styled. Native has no equivalent because there is no document to stream.
  • Pseudo-states. _hover is a no-op on touch surfaces; _focus and _active carry through. Motif does not warn on _hover in a native file because the same component might run on the web through React Native Web.
  • Form primitives. Input, Label, Field, TextArea, and friends ship in the web build. React Native has its own input primitives with different ergonomics; motif's recommendation on native is to wrap them with styled() directly.
  • Overlays. Portal, FocusScope, Overlay, Show, Hide, and VisuallyHidden ship on the web. The native build relies on React Native's modal stack and accessibility APIs instead.

These seams are the price of a single import path. Motif's job is to keep the seams small; the exports condition above is the main mechanism for hiding them.

Write the same code

The shared surface is the point. A team building an app on both platforms does not write Card.web.tsx and Card.native.tsx for the styling — both files are Card.tsx. The places where the platforms genuinely diverge (navigation, gesture handling, network) are the places they diverge. Styling is not one of them.