Create a Design System in Under 15 Minutes
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.
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.
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;
<body>, <h1>–<h6>, <a>, <p>, <code>, <pre>, <ul>, <dl>, and 11 more elements_padding:md, _color:primary, _display:flex, …)_hover:, _focus-visible:, _dark:, _md:, …)Each preset has a single responsibility:
| Preset | What it does |
|---|---|
useDesignTokensPreset | Generates every CSS variable: colors (with auto-levels/shades/tints), spacing, fluid font sizes, line heights, shadows, breakpoints, easings, z-index, and more. |
useSanitizePreset | Applies sanitize.css normalization: box-sizing, margin/padding resets, form-element consistency, system font fallbacks, and prefers-reduced-motion handling. |
useGlobalPreset | Wires up sensible defaults for global HTML elements so plain markup looks designed without any extra classes. |
useUtilitiesPreset | Registers utility class generators for nearly every CSS property: layout, spacing, typography, colors, borders, transforms, transitions, and more. |
useModifiersPreset | Adds pseudo-class, pseudo-element, ARIA, media-query, and directional modifiers so utilities can be combined into stateful and responsive variants. |
Step 2: Brand It (2 minutes)
Pass your brand colors to useDesignTokensPreset. Styleframe automatically generates eleven lightness levels (50–950), four darker shades for hover/active states, and four lighter tints for soft backgrounds, using OKLCH for perceptually uniform output.
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;
:root {
--color--primary: #0066ff;
--color--primary-50: oklch(from var(--color--primary) 0.97 c h);
--color--primary-100: oklch(from var(--color--primary) 0.93 c h);
/* ... all the way through 950 */
--color--primary-shade-50: oklch(from var(--color--primary) calc(l - 0.05) c h);
--color--primary-shade-100: oklch(from var(--color--primary) calc(l - 0.10) c h);
--color--primary-tint-50: oklch(from var(--color--primary) calc(l + 0.05) c h);
--color--primary-tint-100: oklch(from var(--color--primary) calc(l + 0.10) c h);
/* same for secondary, success, warning, error, info */
}
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
:hoverand:activestates. (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)
useDesignTokensPreset(s, {
colors: { brand: '#ff6600' },
});
// All default colors plus `brand`
meta: { merge: false } if you want your custom record to replace the default palette entirely.Learn more about color generation options →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:
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.
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',
},
});
<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.<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.
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.
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
<Button color="success" variant="soft" />, and the recipe resolves it to the right CSS class. Learn more about the Button recipe →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
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.
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
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.
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' },
},
},
});
:root {
--color--primary: #0066ff;
--color--background: #ffffff;
--color--text: #0f172a;
--link--color: var(--color--primary);
--body--color: var(--color--text);
--body--background: var(--color--background);
}
[data-theme="dark"] {
--color--primary: #60a5fa;
--color--background: #0f172a;
--color--text: #f1f5f9;
--link--color: var(--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.
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' } },
},
});
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>
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 →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:
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:
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;
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
themesconfig.
FAQ
useDesignTokensPreset(s) plus useUtilitiesPreset(s), which gives you tokens and utility classes. Add useSanitizePreset for browser normalization, useGlobalPreset for semantic typography, and useModifiersPreset for hover/focus/dark/responsive variants on utilities.Just pass the custom colors — they merge with defaults by default:
useDesignTokensPreset(s, {
colors: { brand: '#ff6600', accent: '#9333ea' },
});
If you want your custom record to replace the default palette entirely, opt out via meta: { merge: false }.
Call any composable on the same Styleframe instance after the preset:
const preset = useDesignTokensPreset(s);
const { colorBrand } = useColorDesignTokens(s, { brand: '#ff00ff' });
The preset doesn't lock the system. It gives you a head start. See the full composables list →
@color.primary, @font-size.sm, and @border-radius.md. The preset is the easiest way to define them, but you can register them manually with the individual composables (useColorDesignTokens, useFontSizeDesignTokens, etc.) if you want fine-grained control over what gets emitted.Pass fluidFontSize: false. The static fontSize domain runs instead, emitting fixed rem values:
useDesignTokensPreset(s, {
fluidFontSize: false,
fontSize: { md: '1rem', lg: '1.25rem' /* ... */ },
});
For a hybrid approach (static everywhere, fluid only on display headings), see the Customizing Fluid Typography example.
Pass false to any of the four categories:
useSanitizePreset(s, {
base: true,
forms: false, // skip form-element normalization
typography: true,
reduceMotion: true,
});
Three levers:
- Disable preset domains you don't need:
useDesignTokensPreset(s, { easing: false, breakpoint: false }). - Skip presets entirely (for example, omit
useUtilitiesPresetif you only use recipes). - On each recipe, pass
filterto limit the generated combinations:useButtonRecipe(s, { filter: { variant: ['solid', 'outline'] } }).
Yes. useUtilitiesPreset returns factory functions for every utility. Call them with extra values:
const { createMarginUtility, createBackgroundUtility } = useUtilitiesPreset(s);
createMarginUtility({ huge: '8rem', tiny: '0.125rem' });
createBackgroundUtility({ pattern: 'url("/dots.svg") repeat' });
For brand-new utilities, define them with s.utility(...). Learn more →
Pass an options object as the second argument. You can override base, defaultVariants, variants, compoundVariants, or filter:
const button = useButtonRecipe(s, {
base: { borderRadius: '@border-radius.full' },
defaultVariants: { color: 'success', variant: 'soft', size: 'lg' },
filter: { variant: ['solid', 'outline'] },
});
Each component's reference page documents its full variant matrix and customization API. Browse the catalog →
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.