API Essentials

Imports

Understand how Styleframe's single-instance architecture works — from the global config, through extension files, to consuming styles and exports in your application code.

Overview

Styleframe uses a single-instance architecture with three layers:

  1. styleframe.config.ts — Creates the global Styleframe instance with your design tokens, presets, and utilities
  2. *.styleframe.ts files — Extend the global instance with component-specific selectors, recipes, and styles
  3. Application code — Consumes the compiled CSS and TypeScript exports via virtual:styleframe
All *.styleframe.ts files share the same Styleframe instance created by your config. There are no independent instances — every file builds on the same foundation.

The Global Config

The styleframe.config.ts file at your project root creates the single Styleframe instance that powers your entire design system.

styleframe.config.ts
import { styleframe } from 'styleframe';

const s = styleframe();
const { variable, utility, recipe, ref } = s;

// Design tokens
const colorPrimary = variable('color.primary', '#3b82f6');
const colorSecondary = variable('color.secondary', '#64748b');
const spacingSm = variable('spacing.sm', '0.5rem');
const spacingMd = variable('spacing.md', '1rem');
const spacingLg = variable('spacing.lg', '1.5rem');

// Utilities
utility('background', ({ value }) => ({ backgroundColor: value }));
utility('padding', ({ value }) => ({ padding: value }));

// Recipes
recipe({
    name: 'button',
    base: { padding: ref(spacingMd) },
    variants: {
        color: {
            primary: { background: ref(colorPrimary) },
            secondary: { background: ref(colorSecondary) },
        },
        size: {
            sm: { padding: ref(spacingSm) },
            md: { padding: ref(spacingMd) },
            lg: { padding: ref(spacingLg) },
        },
    },
});

export default s;

The config file:

  • Imports styleframe from the "styleframe" package (the real npm package)
  • Creates the instance with styleframe()
  • Defines tokens, utilities, recipes, and styles
  • Must export the instance as default (export default s)

Extension Files

Extension files (*.styleframe.ts) add component-specific styles to the global instance. They are co-located with your components and loaded automatically by the plugin.

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

const s = styleframe();
const { variable, recipe, ref } = s;

// Component-specific tokens
const buttonPrimary = variable('button.primary', '#3b82f6');
const buttonSecondary = variable('button.secondary', '#64748b');
const buttonPadding = variable('button.padding', '1rem');

// Component recipe
export const buttonRecipe = recipe({
    name: 'button',
    base: { padding: ref(buttonPadding) },
    variants: {
        color: {
            primary: { background: ref(buttonPrimary) },
            secondary: { background: ref(buttonSecondary) },
        },
        size: {
            sm: { padding: ref('spacing.sm') },
            md: { padding: ref('spacing.md') },
            lg: { padding: ref('spacing.lg') },
        },
    },
});

export default s;
Extension files must import from "virtual:styleframe", not "styleframe". Using the real package would create a separate, disconnected instance that is not part of your design system.
You do not need to re-register tokens in extension files. Since they share the global instance, all variables defined in the config are available via ref().

Consuming Styles

Application code consumes the compiled output through two imports:

ImportWhat it provides
import 'virtual:styleframe.css'All compiled CSS — variables, selectors, utilities, and recipe classes
import { ... } from 'virtual:styleframe'Runtime exports — recipe functions and selector strings
src/main.ts
// 1. Import all compiled CSS (typically once, at your app entry point)
import 'virtual:styleframe.css';

// 2. Import recipe functions and selector strings where needed
import { buttonRecipe, alertRecipe, cardSelector } from 'virtual:styleframe';

// Use recipes to generate class names at runtime
const className = buttonRecipe({ color: 'primary', size: 'md' });

// Use selector strings for DOM access
const cards = document.querySelectorAll(cardSelector);
The same "virtual:styleframe" import path works differently depending on who imports it. Extension files (*.styleframe.ts) receive the raw Styleframe instance, while application code receives the compiled recipe functions and selector strings.
If you're not using recipes or selector exports, you only need the CSS import. The TypeScript import is for runtime functions and constants that you reference in your JavaScript/TypeScript code.

Named Exports

Named exports from extension files control what's available to your application code through "virtual:styleframe".

Recipe Exports

When you assign a recipe to a named export, that name is preserved in the generated output:

button.styleframe.ts
import { styleframe } from 'virtual:styleframe';

const s = styleframe();

// Named export — uses your chosen name
export const primaryButton = s.recipe({
    name: 'button',
    base: { cursor: 'pointer' },
    variants: {
        size: {
            sm: { padding: '0.5rem' },
            lg: { padding: '1rem' },
        },
    },
});

export default s;

Without a named export, the export name is derived from the recipe's name property (converted to camelCase):

// Without named export
s.recipe({ name: 'primary-button', /* ... */ });

// Generated export name: primaryButton (derived from recipe name)

Selector Exports

You can export selectors as string constants for programmatic DOM access or testing:

card.styleframe.ts
import { styleframe } from 'virtual:styleframe';

const s = styleframe();

export const cardSelector = s.selector('.card', {
    padding: '1rem',
    borderRadius: '8px',
    boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
});

export const cardHeaderSelector = s.selector('.card-header', {
    fontWeight: 'bold',
    marginBottom: '0.5rem',
});

export default s;
Selectors require named exports to be included in the "virtual:styleframe" TypeScript output — without a named export, they generate CSS only. Recipes are always included, using either the named export name or an auto-derived name from the recipe's name property. The export default s is required in your config file but optional in extension files.

Combining Recipes and Selectors

A common pattern is to export both recipes (for variant-based styling) and selectors (for static styles or DOM access):

button.styleframe.ts
import { styleframe } from 'virtual:styleframe';

const s = styleframe();
const { variable, ref } = s;

const buttonPadding = variable('button.padding', '1rem');

// Static base styles via selector
export const buttonBase = s.selector('.btn', {
    display: 'inline-flex',
    alignItems: 'center',
    borderRadius: '4px',
});

// Variant styles via recipe
export const button = s.recipe({
    name: 'button',
    base: { padding: ref(buttonPadding) },
    variants: {
        variant: {
            primary: { background: 'blue', color: 'white' },
            secondary: { background: 'gray', color: 'black' },
        },
    },
});

export default s;
Button.tsx
import { buttonBase, button } from 'virtual:styleframe';

export function Button({ variant, children }) {
    // Combine base selector class with recipe variant classes
    return (
        <button className={`${buttonBase.slice(1)} ${button({ variant })}`}>
            {children}
        </button>
    );
}

Framework Usage

import { useMemo } from 'react';
import { button } from 'virtual:styleframe';

interface ButtonProps {
    color?: 'primary' | 'secondary';
    size?: 'sm' | 'md' | 'lg';
    children: React.ReactNode;
}

export function Button({ color, size, children }: ButtonProps) {
    const className = useMemo(() => button({ color, size }), [color, size]);

    return (
        <button className={className}>
            {children}
        </button>
    );
}
Import 'virtual:styleframe.css' once at your application entry point (e.g., main.ts or app.ts). You do not need to import it in every component file.

FAQ