Recipes

Migrate from Tailwind

To move a codebase from Tailwind to motif, translate utility classes into style props and port tailwind.config into a theme. The scales line up — Tailwind's spacing and breakpoints are the same shape as motif's token tree.

Updated 3 days agoEdit on GitHubWeb & native

What maps directly

Tailwindmotif
flex gap-4display="flex" gap="$4"
p-4 / px-3 / mt-2p="$4" / px="$3" / mt="$2"
bg-white / text-gray-900bg="$colors.surface.base" / color="$colors.text.default"
rounded-lgborderRadius="$radii.lg"
text-lg font-semiboldfontSize="$lg" fontWeight="$semibold"
hover:bg-gray-100_hover={{ bg: '$colors.surface.muted' }}
md:flex-rowflexDirection={{ base: 'column', md: 'row' }}
dark:bg-blacka dark theme — see Theming
tailwind.config themecreateTheme({ tokens: { … } })

Tailwind's numeric spacing scale (4 = 1rem) and motif's space scale are the same idea — a named step, not a raw length. The $4 reference resolves through the theme exactly as p-4 runs through the config.

Mismatches and gaps

  • Arbitrary-value classes. Tailwind's p-[13px] escape hatch becomes a literal prop value: p={13} or p="13px". motif accepts literals anywhere a token reference is valid.
  • @apply. Tailwind's @apply composes utilities inside a CSS rule. motif's equivalent is styled() — bundle the recurring set of props into one named component.
  • The plugin ecosystem. @tailwindcss/forms, @tailwindcss/typography, and other plugins have no motif equivalent. motif's form primitives cover the forms case; prose styling is a styled() component you own.
  • Dark mode. Tailwind's dark: variant toggles a class; motif models light and dark as two themes and switches the active one. The result is the same; the mechanism is a theme, not a per-utility variant.
  • JIT and purging. Tailwind ships a build step that scans class names. motif's CSS comes from the runtime or the compiler — there is no class-name scan, so no purge config and no safelist.

Migrate a component

A typical Tailwind component:

example.tsx
// before — Tailwind
function Card({ title, body }: { title: string; body: string }) {
return (
  <article className="flex flex-col gap-3 p-4 rounded-lg bg-white hover:bg-gray-50 md:p-6">
    <h3 className="text-lg font-semibold text-gray-900">{title}</h3>
    <p className="text-sm text-gray-600">{body}</p>
  </article>
);
}

Becomes:

example.tsx
// after — motif
import { Box, Text } from 'usemotif';
 
function Card({ title, body }: { title: string; body: string }) {
return (
  <Box
    as="article"
    display="flex"
    flexDirection="column"
    gap="$3"
    p={{ base: '$4', md: '$6' }}
    borderRadius="$radii.lg"
    bg="$colors.surface.base"
    _hover={{ bg: '$colors.surface.muted' }}
  >
    <Text as="h3" fontSize="$lg" fontWeight="$semibold" color="$colors.text.default">
      {title}
    </Text>
    <Text fontSize="$sm" color="$colors.text.muted">{body}</Text>
  </Box>
);
}

The responsive md:p-6 becomes a responsive object — p={{ base: '$4', md: '$6' }}. The grey literals become semantic tokens; pick the theme path that carries the intent (surface, text.muted) rather than transcribing the shade number.

Migrate the config

tailwind.config becomes a createTheme call. The theme object is the token tree.

example.tsx
// before — tailwind.config.js
module.exports = {
theme: {
  colors: { white: '#fff', gray: { 50: '#F9FAFB', 600: '#4B5563', 900: '#111827' } },
  spacing: { 3: '12px', 4: '16px', 6: '24px' },
  borderRadius: { lg: '8px' },
},
};
 
// after — motif theme
import { createTheme } from 'usemotif';
 
export const theme = createTheme({
name: 'light',
tokens: {
  colors: {
    surface: { base: '#FFFFFF', muted: '#F9FAFB' },
    text: { default: '#111827', muted: '#4B5563' },
  },
  space: { 3: 12, 4: 16, 6: 24 },
  radii: { lg: 8 },
},
});

This is the moment to add a semantic layer. Tailwind's gray-600 is a value; motif's text.muted is a role. Naming the roles once, in the theme, is what lets a dark theme drop in later without touching a component.

Migrate incrementally

A motif <Box className="…"> still passes className straight through, so a half-migrated component can carry Tailwind classes and motif props at once. Convert file by file; uninstall Tailwind when grep className turns up nothing but motif primitives.