Getting started

Your first style

You'll go from a blank app to a themed, hover-reactive card — and then promote it into a reusable component with two visual tones. Six steps, ten minutes.

Updated 3 days agoEdit on GitHubWeb & native

What you'll build

A <Card> that takes a tone prop. tone="neutral" is cream paper on ink text; tone="warning" is ochre on ink. Hover deepens the surface. The component reads everything from the active theme — no hex codes at the call site.

You'll write the theme first, then a one-off styled <Box>, then the reusable component. Each step builds on the previous one.

  1. Define a theme

    Create theme.ts next to your entry file. A theme is a name plus a token tree.

    theme.ts
    import { createTheme } from 'usemotif';
     
    export const lightTheme = createTheme({
    name: 'light',
    tokens: {
      colors: {
        paper: '#FBF7F2',
        paperDeep: '#F1ECE2',
        ochre: '#E8D7A6',
        ochreDeep: '#DCC58B',
        ink: '#1C1917',
        surface: {
          neutral: '$colors.paper',
          neutralHover: '$colors.paperDeep',
          warning: '$colors.ochre',
          warningHover: '$colors.ochreDeep',
        },
        text: { default: '$colors.ink' },
      },
      space: { 2: 8, 3: 12, 4: 16, 6: 24 },
      radii: { md: 8, lg: 12 },
    },
    });
    

    The surface.neutral / surface.warning paths are semantic — they reference primitives by $, so swapping themes later is just changing the right side of those references.

  2. Mount the provider

    Wrap the root of your app once.

    main.tsx
    import { ThemeProvider } from 'usemotif';
    import { lightTheme } from './theme';
    import { App } from './App';
     
    export function Root() {
    return (
      <ThemeProvider themes={[lightTheme]} active="light">
        <App />
      </ThemeProvider>
    );
    }
    

    The provider emits a <style> element with every theme's CSS variables and sets data-theme="light" on a wrapping <div>. Everything below that node can read tokens.

  3. Render a Box

    In App.tsx, render a single <Box> that reads the surface and text tokens.

    example.tsx
    import { Box } from 'usemotif';
     
    export function App() {
    return (
      <Box
        bg="$colors.surface.neutral"
        color="$colors.text.default"
        p="$space.6"
        borderRadius="$radii.lg"
      >
        <strong>A surface.</strong>
        <p>Cream paper on ink text — straight from the theme.</p>
      </Box>
    );
    }
    

    You should now see a cream rectangle. The bg, color, p, borderRadius props are typed against the active theme; each $-reference compiles to a var(--…) lookup at render.

  4. Add a hover response

    The _hover prop accepts a style bag that applies on :hover. Add it to deepen the surface.

    example.tsx
    <Box
    bg="$colors.surface.neutral"
    color="$colors.text.default"
    p="$space.6"
    borderRadius="$radii.lg"
    _hover={{ bg: '$colors.surface.neutralHover' }}
    >
    
    </Box>
    

    Hover the rectangle — the colour deepens. Motif emits one CSS rule for the hover state, hashes it into a class, and dedupes across the page. The same component on a hundred pages produces the same class.

  5. Promote to a styled component

    The component will be reused with two tones. Move the styles into a styled() call and add a tone variant.

    Card.tsx
    import { styled } from 'usemotif';
     
    export const Card = styled('section', {
    base: {
      color: '$colors.text.default',
      p: '$space.6',
      borderRadius: '$radii.lg',
    },
    variants: {
      tone: {
        neutral: {
          bg: '$colors.surface.neutral',
          _hover: { bg: '$colors.surface.neutralHover' },
        },
        warning: {
          bg: '$colors.surface.warning',
          _hover: { bg: '$colors.surface.warningHover' },
        },
      },
    },
    defaultVariants: { tone: 'neutral' },
    });
    

    <Card> now accepts an optional tone prop with two legal values. The component still takes every Box prop (p, bg, borderRadius, …), so call sites can override anything.

  6. Use it

    Back in App.tsx, render two cards — one of each tone.

    example.tsx
    import { Card } from './Card';
     
    export function App() {
    return (
      <>
        <Card>
          <strong>Default.</strong>
          <p>Cream paper, deepens on hover.</p>
        </Card>
        <Card tone="warning">
          <strong>Warning.</strong>
          <p>Ochre surface, deepens on hover.</p>
        </Card>
      </>
    );
    }
    

    Both cards share the base padding, radius, and text colour. Only the surface tokens differ. Add a third tone (success, danger) by adding one entry to the variants map.

What just happened

You wrote one set of tokens, one provider, and one component — and got a typed, themed, hover-reactive surface that scales with the design.

A few moving parts to keep in mind:

  • The theme is the source of truth. Every colour and spacing on the page came from the tokens tree. Swapping in a darkTheme would change every value at once.
  • <Box> is the atom. Style props, token references, pseudo-state bags. Every primitive in the library and every styled() component renders through <Box> underneath.
  • styled() does not invent a new concept. It builds a regular React component that applies the same style-prop pipeline you already used inline.

Next