Create a Design System in Under 15 Minutes

Build a complete, production-ready design system with Styleframe. Stand up tokens, themes, semantic typography, utilities, and ready-made component recipes – all type-safe and theme-aware.

Building a design system from scratch can take weeks. Picking color scales, tuning typography for every viewport, wiring up dark mode, hand-rolling each component – it adds up fast.

Styleframe collapses all of that into five preset calls. Five lines of configuration give you a complete, type-safe design token system with sensible defaults, opinionated typography, a CSS reset, ~200 utility classes, and modifier syntax for hover/focus/dark/responsive states. Then you customize what you want, plug in 23 production-ready component recipes, and ship.

What you'll build: A full design system with branded colors (and auto-generated levels, shades, and tints), fluid typography that scales smoothly across viewports, dark mode, semantic global element styling, ready-to-use Button and Card components, plus utility classes and modifiers. All type-safe at build time.

Prerequisites

Before starting, make sure you have:

  • Node.js 18+
  • A project with the Styleframe Vite plugin already configured (Manual Vite Installation)
  • Familiarity with TypeScript is helpful but not required

Step 1: The Foundation (1 minute)

Open your styleframe.config.ts and add the five presets that ship with the @styleframe/theme package. This is the canonical setup that every Styleframe app starts from.

styleframe.config.ts
import {
    useDesignTokensPreset,
    useGlobalPreset,
    useModifiersPreset,
    useSanitizePreset,
    useUtilitiesPreset,
} from '@styleframe/theme';
import { styleframe } from 'styleframe';

const s = styleframe();

useDesignTokensPreset(s);
useSanitizePreset(s);
useGlobalPreset(s);
useUtilitiesPreset(s);
useModifiersPreset(s);

export default s;

Each preset has a single responsibility:

PresetWhat it does
useDesignTokensPresetGenerates every CSS variable: colors (with auto-levels/shades/tints), spacing, fluid font sizes, line heights, shadows, breakpoints, easings, z-index, and more.
useSanitizePresetApplies sanitize.css normalization: box-sizing, margin/padding resets, form-element consistency, system font fallbacks, and prefers-reduced-motion handling.
useGlobalPresetWires up sensible defaults for global HTML elements so plain markup looks designed without any extra classes.
useUtilitiesPresetRegisters utility class generators for nearly every CSS property: layout, spacing, typography, colors, borders, transforms, transitions, and more.
useModifiersPresetAdds pseudo-class, pseudo-element, ARIA, media-query, and directional modifiers so utilities can be combined into stateful and responsive variants.
You can stop here. Even with zero customization, your app already has a CSS reset, a full token system, semantic typography, and a deep utility library. Every following step is opt-in customization.

Step 2: Brand It (2 minutes)

Pass your brand colors to useDesignTokensPreset. Styleframe automatically generates eleven lightness levels (50950), four darker shades for hover/active states, and four lighter tints for soft backgrounds, using OKLCH for perceptually uniform output.

styleframe.config.ts
import { useDesignTokensPreset /* ... */ } from '@styleframe/theme';
import { styleframe } from 'styleframe';

const s = styleframe();

useDesignTokensPreset(s, {
    colors: {
        primary: '#0066ff',
        secondary: '#7c3aed',
        success: '#10b981',
        warning: '#f59e0b',
        error: '#ef4444',
        info: '#06b6d4',
    },
});

// ... other presets
export default s;

Use these in your CSS and components:

  • Levels: full lightness ramp for backgrounds, text, and borders. (primary-50, primary-100, primary-200, etc.)
  • Shades: subtle darken steps, perfect for :hover and :active states. (primary-shade-50, primary-shade-100, primary-shade-150)
  • Tints: subtle lighten steps, perfect for soft backgrounds and badges. (primary-tint-50, primary-tint-100, primary-tint-150)
Add without replacing. Custom values merge with defaults by default, so just list the colors you want to add:
useDesignTokensPreset(s, {
    colors: { brand: '#ff6600' },
});
// All default colors plus `brand`
Pass meta: { merge: false } if you want your custom record to replace the default palette entirely.Learn more about color generation options →
Why OKLCH? Unlike HSL, OKLCH maintains perceptual uniformity. Lightness 0.5 actually looks halfway between black and white across every hue, so color ramps look consistent and contrast scales predictably.

Tune Typography

The preset enables fluid typography by default: every font-size.* token interpolates smoothly between a min size at the smallest viewport and a max size at the largest. No media queries required. Type just grows.

You can override the viewport range, the modular scale ratios, and the base font size:

styleframe.config.ts
useDesignTokensPreset(s, {
    colors: { /* ... */ },

    // Custom font stack
    fontFamily: {
        default: '@font-family.base',
        base: '"Inter", -apple-system, BlinkMacSystemFont, sans-serif',
        mono: '"JetBrains Mono", SFMono-Regular, Menlo, monospace',
    },

    // Tune the fluid viewport range and modular-scale ratios.
    fluidViewport: { minWidth: 375, maxWidth: 1440 },
    fluidScale: { min: '@scale.minor-third', max: '@scale.major-third' },

    // Retarget the fluid base (defaults: min 16 / max 18).
    fontSize: { 
        min: 16, 
        max: 20,
    },
});

Learn more about fluid typography →

Step 3: Polish the Globals (1 minute)

useGlobalPreset styles native HTML elements so plain markup (from a CMS, an email, a third-party widget) already looks designed.

styleframe.config.ts
useGlobalPreset(s, {
    body: {
        color: '@color.text',
        background: '@color.background',
        lineHeight: '@line-height.normal',
    },
    heading: {
        fontWeight: '@font-weight.bold',
        lineHeight: '@line-height.tight',
        sizes: {
            h1: '@font-size.4xl',
            h2: '@font-size.3xl',
            h3: '@font-size.2xl',
            h4: '@font-size.xl',
            h5: '@font-size.lg',
            h6: '@font-size.md',
        },
    },
    link: {
        color: '@color.primary',
        textDecoration: 'none',
        hoverColor: '@color.primary-700',
        hoverTextDecoration: 'underline',
    },
});
The Global preset also covers <p>, <code>, <pre>, <ul>, <ol>, <dl>, <dt>, <dd>, <hr>, <kbd>, <mark>, <abbr>, <samp>, <address>, <caption>, plus selection and focus states. Pass false to any element key (e.g., link: false) to opt a specific element out.
Form elements like <input>, <select>, <textarea>, and <button> are normalized by useSanitizePreset (the forms category). To opt out, pass useSanitizePreset(s, { forms: false }).

Step 4: Build a Button Component (3 minutes)

Token configuration takes you most of the way, but real apps need real components. That's what recipes are for: pre-built variant systems that compile to type-safe CSS classes at build time, with zero runtime overhead.

The useButtonRecipe composable ships 9 colors × 6 visual styles × 5 sizes = 270 type-safe combinations, including hover, focus, active, disabled, and dark-mode states.

Register the recipe

Recipes get registered alongside your component, in a co-located *.styleframe.ts file. The Vite plugin discovers it automatically.

src/components/button.styleframe.ts
import { useButtonRecipe } from '@styleframe/theme';
import { styleframe } from 'virtual:styleframe';

const s = styleframe();

const button = useButtonRecipe(s);

export default s;

Build the component

Import the button runtime function from the auto-generated virtual:styleframe module and pass variant props to compute class names. The function is fully typed, so invalid colors, variants, or sizes are caught at compile time.

src/components/Button.tsx
import { button } from 'virtual:styleframe';

interface ButtonProps {
    color?: 'primary' | 'secondary' | 'success' | 'info' | 'warning' | 'error' | 'light' | 'dark' | 'neutral';
    variant?: 'solid' | 'outline' | 'soft' | 'subtle' | 'ghost' | 'link';
    size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
    disabled?: boolean;
    children?: React.ReactNode;
}

export function Button({
    color = 'neutral',
    variant = 'solid',
    size = 'md',
    disabled = false,
    children,
}: ButtonProps) {
    return (
        <button className={button({ color, variant, size })} disabled={disabled}>
            {children}
        </button>
    );
}

See it in action

Compound variants do the heavy lifting. Each color-variant pairing has its own background, text, and border colors plus hover/focus/active overrides. You write <Button color="success" variant="soft" />, and the recipe resolves it to the right CSS class. Learn more about the Button recipe →
Trim what you don't need. Pass filter to limit which variants are generated. The unused combinations are pruned from the output CSS:
const button = useButtonRecipe(s, {
    filter: {
        color: ['primary', 'error'],
        variant: ['solid', 'outline'],
    },
});

Step 5: Compose a Card (2 minutes)

Some components have multiple structural parts. The Card recipe ships four coordinated composables that share the same color, variant, and size axes, so the parts always look like they belong together: useCardRecipe (the container), useCardHeaderRecipe, useCardBodyRecipe, and useCardFooterRecipe.

Register the recipes

src/components/card.styleframe.ts
import {
    useCardRecipe,
    useCardHeaderRecipe,
    useCardBodyRecipe,
    useCardFooterRecipe,
} from '@styleframe/theme';
import { styleframe } from 'virtual:styleframe';

const s = styleframe();

const card = useCardRecipe(s);
const cardHeader = useCardHeaderRecipe(s);
const cardBody = useCardBodyRecipe(s);
const cardFooter = useCardFooterRecipe(s);

export default s;

Compose the component

Drop a Button inside the footer to combine recipes. Layout is handled with utility classes from useUtilitiesPreset. No extra CSS file required.

src/components/ProfileCard.tsx
import { card, cardHeader, cardBody, cardFooter } from 'virtual:styleframe';
import { Button } from './Button';

export function ProfileCard() {
    return (
        <div className={card({ color: 'neutral', variant: 'solid', size: 'md' })}>
            <div className={cardHeader({ color: 'neutral', variant: 'solid', size: 'md' })}>
                <strong>Acme Inc.</strong>
            </div>
            <div className={cardBody({ size: 'md' })}>
                <p className="_margin:0">Manage your team's design system tokens, themes, and components in one place.</p>
            </div>
            <div className={`${cardFooter({ color: 'neutral', variant: 'solid', size: 'md' })} _display:flex _gap:sm _justify-content:flex-end`}>
                <Button color="neutral" variant="ghost">Cancel</Button>
                <Button color="primary" variant="solid">Save</Button>
            </div>
        </div>
    );
}

See it in action

Twenty-one more recipes are ready to drop in. Badge, Callout, Chip, Modal, Popover, Tooltip, Input, Spinner, Skeleton, Placeholder, Pagination, Breadcrumb, Nav, Dropdown, Hamburger Menu, Page Hero, Progress, Media, Button Group, and the Card sub-parts. Browse the full component catalog →

Step 6: Add Dark Mode (2 minutes)

Dark mode in Styleframe is a configuration, not a refactor. Pass a themes map to useDesignTokensPreset and the preset emits a [data-theme="dark"] block that overrides only the tokens you specify. Every recipe and utility that references those tokens picks up the new values automatically.

styleframe.config.ts
useDesignTokensPreset(s, {
    colors: {
        primary: '#0066ff',
        background: '#ffffff',
        text: '#0f172a',
    },
    themes: {
        dark: {
            colors: {
                primary: '#60a5fa',
                background: '#0f172a',
                text: '#f1f5f9',
            },
        },
    },
});

useGlobalPreset(s, {
    body: { color: '@color.text', background: '@color.background' },
    link: {
        color: '@color.primary',
        themes: {
            dark: { color: '@color.primary-tint-50' },
        },
    },
});

To toggle the theme, set the data-theme attribute on <html>:

document.documentElement.dataset.theme = 'dark';

That's it. Every CSS variable updates instantly, no page reload, no flicker.

Production-ready theme switching (including system-preference detection, localStorage persistence, smooth transitions, and FOUC prevention) is covered in the dedicated Theme Switcher Guide.

You can define more than one theme and override anything: colors, spacing, typography, breakpoints, easings, font stacks. Useful for compact UIs, high-contrast accessibility modes, or per-tenant branding.

useDesignTokensPreset(s, {
    themes: {
        dark: { colors: { primary: '#60a5fa' } },
        compact: { spacing: { default: '0.75rem', md: '0.75rem', lg: '1.25rem' } },
        'high-contrast': { colors: { primary: '#0000ff', text: '#000000' } },
    },
});

Learn more about themes →

Step 7: Style on the Fly with Utilities & Modifiers (1 minute)

For one-off styling that doesn't deserve a recipe (spacing tweaks, layout, color overrides), reach for utility classes. They're generated from your design tokens, so a change to colors.primary ripples through every utility automatically.

<!-- Layout -->
<div class="_display:flex _gap:md _padding:lg _align-items:center">
    <h2 class="_margin:0 _color:primary">Welcome back</h2>
    <button class="_margin-left:auto _padding:sm _background:primary-tint-100">
        Sign out
    </button>
</div>

<!-- One-off values -->
<section class="_max-width:[640px] _margin-inline:auto _padding-block:[3rem]">
    <p class="_color:text-weak _line-height:relaxed">
        Bracket syntax handles values that aren't in your design tokens.
    </p>
</section>

Add a modifier prefix for state and responsive variants. The format is _<modifier>:<property>:<value>:

<a class="_color:primary _hover:color:primary-700 _focus-visible:outline:[2px_solid_currentColor]">
    Read more
</a>

<div class="_background:white _dark:background:gray-900 _padding:md _md:padding:xl">
    Adapts to dark mode and grows on tablet+.
</div>
Modifiers are also generated by useModifiersPreset: pseudo-classes (_hover:, _focus-visible:, _active:, _disabled:), pseudo-elements (_before:, _after:, _placeholder:), media queries (_sm:_2xl:, _dark:, _print:, _motion-safe:), ARIA states (_aria-expanded:, _aria-disabled:), and structural selectors (_first-child:, _odd:, etc.). Learn more about utility modifiers →

Learn more about utilities →

Putting It All Together

Here's the complete styleframe.config.ts from this guide, with brand colors, custom typography, dark mode, semantic globals, utilities, and modifiers all in one place:

styleframe.config.ts
import {
    useDesignTokensPreset,
    useGlobalPreset,
    useModifiersPreset,
    useSanitizePreset,
    useUtilitiesPreset,
} from '@styleframe/theme';
import { styleframe } from 'styleframe';

const s = styleframe();

useDesignTokensPreset(s, {
    colors: {
        primary: '#0066ff',
        secondary: '#7c3aed',
        success: '#10b981',
        warning: '#f59e0b',
        error: '#ef4444',
        info: '#06b6d4',
        background: '#ffffff',
        text: '#0f172a',
    },
    fontFamily: {
        default: '@font-family.base',
        base: '"Inter", -apple-system, BlinkMacSystemFont, sans-serif',
        mono: '"JetBrains Mono", SFMono-Regular, Menlo, monospace',
    },
    fluidViewport: { minWidth: 375, maxWidth: 1440 },
    themes: {
        dark: {
            colors: {
                primary: '#60a5fa',
                background: '#0f172a',
                text: '#f1f5f9',
            },
        },
    },
});

useSanitizePreset(s);

useGlobalPreset(s, {
    body: { color: '@color.text', background: '@color.background' },
    heading: {
        fontWeight: '@font-weight.bold',
        lineHeight: '@line-height.tight',
        sizes: {
            h1: '@font-size.4xl',
            h2: '@font-size.3xl',
            h3: '@font-size.2xl',
            h4: '@font-size.xl',
            h5: '@font-size.lg',
            h6: '@font-size.md',
        },
    },
    link: {
        color: '@color.primary',
        textDecoration: 'none',
        hoverColor: '@color.primary-700',
        hoverTextDecoration: 'underline',
        themes: {
            dark: { color: '@color.primary-tint-50' },
        },
    },
});

useUtilitiesPreset(s);
useModifiersPreset(s);

export default s;

And the matching co-located component file:

src/components/app.styleframe.ts
import {
    useButtonRecipe,
    useCardRecipe,
    useCardHeaderRecipe,
    useCardBodyRecipe,
    useCardFooterRecipe,
    useBadgeRecipe,
} from '@styleframe/theme';
import { styleframe } from 'virtual:styleframe';

const s = styleframe();

const button = useButtonRecipe(s);
const card = useCardRecipe(s);
const cardHeader = useCardHeaderRecipe(s);
const cardBody = useCardBodyRecipe(s);
const cardFooter = useCardFooterRecipe(s);
const badge = useBadgeRecipe(s);

export default s;
This is everything. Around 60 lines of configuration give you a full design system: tokens, themes, semantic typography, utilities, modifiers, and ready-to-use components. The Styleframe transpiler turns it into static CSS classes at build time with zero runtime overhead.

Why This Works

Building a design system the traditional way means:

  • Hand-defining dozens of color variations
  • Writing media queries for every responsive type step
  • Crafting hover/focus/active CSS by hand for every component
  • Maintaining a parallel [data-theme="dark"] stylesheet
  • Building components from raw CSS or fighting a runtime CSS-in-JS library
  • Constantly chasing token drift between design and code

With Styleframe presets and recipes you get:

  • Auto-generated color levels, shades, and tints in perceptually uniform OKLCH
  • Fluid typography out of the box, with smooth scaling and no media queries
  • Compound variants that resolve color × variant pairings into the right hover/focus/active/dark-mode CSS automatically
  • Configuration-based theming via themes: { dark: {...} } instead of separate stylesheets
  • Library of production-ready recipes with type-safe props
  • Zero runtime overhead, since everything compiles to static CSS at build time
  • One source of truth, with design tokens cascading through utilities, recipes, and global elements

Next Steps

  • Customize deeper: Design Tokens Preset API covers every option, every default, every override.
  • Browse components: the Components catalog lists Button, Card, Modal, Tooltip, Popover, Input, and 17 more, each with its own variant reference.
  • Implement a theme switcher: the Theme Switcher Guide covers system preference detection, persistence, and FOUC prevention.
  • Author your own recipes: the Recipes API lets you build type-safe variant systems for your custom components.
  • Master utilities: Utilities API and Utility Modifiers API.
  • Multi-theme apps: the Themes API exposes the runtime theming primitives behind the preset's themes config.

FAQ


Congratulations. You now have a complete, production-ready design system (type-safe tokens, fluid typography, dark mode, semantic global styling, ~200 utility classes, modifier syntax, and ready-to-use components) in under 15 minutes. Now go ship something.