API Essentials

Composables

Composables are design patterns for building reusable, shareable design system building blocks. They provide idempotency, configurability, and composition for variables, selectors, and recipes.

Overview

Composables are design patterns for organizing and reusing Styleframe primitives (variables, selectors, utilities, recipes). While you can use these APIs directly in your config file, composables add three key capabilities:

  1. Idempotency - Safe to call multiple times without overwriting values
  2. Configurability - Accept options with sensible defaults
  3. Composition - Build complex systems from simpler building blocks

Why use composables?

Use the Styleframe APIs directly when you're defining styles in a single config file for a single project. Use composables when you need to:

  • Share design tokens across projects - Package your variables as composables that can be imported anywhere
  • Build configurable components - Create selectors and recipes that accept options while providing sensible defaults
  • Prevent accidental overwrites - The default: true option ensures variables aren't redefined when composables are called multiple times
  • Return typed references - Functions return typed variable references that other code can safely use

The Idempotency Pattern

The idempotency pattern ensures that composables can be called multiple times without side effects. This is critical for design tokens that might be imported from multiple places.

The Problem

Without idempotency, calling a composable twice would redefine variables:

// This would define --spacing twice!
useSpacingVariables(s);
useSpacingVariables(s); // Overwrites the first definition

The Solution

Use default: true when defining variables in composables. This ensures the variable is only set if it doesn't already exist:

useSpacingVariables.ts
import type { Styleframe } from "styleframe";

export function useSpacingVariables(s: Styleframe) {
    const { variable, ref, css } = s;

    const spacing = variable("spacing", "1rem", { default: true });
    const spacingSm = variable(
        "spacing.sm",
        css`calc(${ref(spacing)} * 0.5)`,
        { default: true }
    );
    const spacingMd = variable("spacing.md", ref(spacing), { default: true });
    const spacingLg = variable(
        "spacing.lg",
        css`calc(${ref(spacing)} * 1.5)`,
        { default: true }
    );

    return { spacing, spacingSm, spacingMd, spacingLg };
}

Type-Safe Returns

The returned object provides typed references that can be used elsewhere. This creates a contract: any code importing the composable gets autocomplete and type checking for the available tokens.

// In another file
import { useSpacingVariables } from "./useSpacingVariables";

const { spacingMd, spacingLg } = useSpacingVariables(s);
// ^ TypeScript knows exactly what's available

The Configuration Pattern

The configuration pattern allows composables to accept options while providing sensible defaults. This makes your design system flexible without sacrificing ease of use.

Configurable Selectors

Create selectors that can be customized per-project while working out of the box:

useCardSelectors.ts
import type { Styleframe } from "styleframe";
import { useSpacingVariables } from "./useSpacingVariables";

interface CardOptions {
    padding?: "sm" | "md" | "lg";
    borderRadius?: string;
}

export function useCardSelectors(
    s: Styleframe,
    options: CardOptions = {}
) {
    const { selector, ref } = s;
    const { padding = "md", borderRadius = "0.5rem" } = options;

    const spacing = useSpacingVariables(s);
    const paddingRef = {
        sm: ref(spacing.spacingSm),
        md: ref(spacing.spacingMd),
        lg: ref(spacing.spacingLg),
    }[padding];

    selector(".card", {
        padding: paddingRef,
        borderRadius,
        backgroundColor: "white",
        boxShadow: "0 1px 3px rgba(0, 0, 0, 0.1)",

        ".card-title": {
            fontSize: "1.25rem",
            fontWeight: "600",
            marginBottom: ref(spacing.spacingSm),
        },

        ".card-content": {
            lineHeight: "1.6",
        },
    });
}

Configurable Recipes

Create recipes that generate only the variants you need:

useButtonRecipe.ts
import type { Styleframe } from "styleframe";
import { useColorVariables } from "./useColorVariables";

type ButtonColor = "primary" | "secondary" | "danger";
type ButtonSize = "sm" | "md" | "lg";

interface ButtonOptions {
    colors?: ButtonColor[];
    sizes?: ButtonSize[];
}

export function useButtonRecipe(
    s: Styleframe,
    options: ButtonOptions = {}
) {
    const { recipe, ref } = s;
    const {
        colors = ["primary", "secondary"],
        sizes = ["sm", "md", "lg"],
    } = options;

    // Get color tokens from the color variables composable
    const { colorPrimary, colorSecondary, colorDanger } = useColorVariables(s);

    const colorVariants: Record<string, object> = {};
    const sizeVariants: Record<string, object> = {};

    // Build only the color variants requested
    if (colors.includes("primary")) {
        colorVariants.primary = {
            backgroundColor: ref(colorPrimary),
            color: "white",
        };
    }
    if (colors.includes("secondary")) {
        colorVariants.secondary = {
            backgroundColor: ref(colorSecondary),
            color: "white",
        };
    }
    if (colors.includes("danger")) {
        colorVariants.danger = {
            backgroundColor: ref(colorDanger),
            color: "white",
        };
    }

    // Build only the size variants requested
    if (sizes.includes("sm")) {
        sizeVariants.sm = { padding: "0.25rem 0.5rem", fontSize: "0.875rem" };
    }
    if (sizes.includes("md")) {
        sizeVariants.md = { padding: "0.5rem 1rem", fontSize: "1rem" };
    }
    if (sizes.includes("lg")) {
        sizeVariants.lg = { padding: "0.75rem 1.5rem", fontSize: "1.125rem" };
    }

    recipe({
        name: "button",
        base: {
            borderRadius: "0.25rem",
            border: "none",
            cursor: "pointer",
            fontWeight: "500",
        },
        variants: {
            color: colorVariants,
            size: sizeVariants,
        },
        defaultVariants: {
            color: "primary",
            size: "md",
        },
    });
}

The Composition Pattern

The composition pattern lets you build complex design systems from simpler composables. A single entry point can orchestrate your entire design system.

useDesignSystem.ts
import type { Styleframe } from "styleframe";

// Import your composables
import { useColorVariables } from "./useColorVariables";
import { useSpacingVariables } from "./useSpacingVariables";
import { useTypographyVariables } from "./useTypographyVariables";
import { useButtonRecipe } from "./useButtonRecipe";
import { useCardSelectors } from "./useCardSelectors";

interface DesignSystemOptions {
    includeComponents?: boolean;
}

export function useDesignSystem(
    s: Styleframe,
    options: DesignSystemOptions = {}
) {
    const { includeComponents = true } = options;

    // Foundation: Design tokens (order matters for dependencies)
    const colors = useColorVariables(s);
    const spacing = useSpacingVariables(s);
    const typography = useTypographyVariables(s);

    // Components: Built on top of tokens
    if (includeComponents) {
        useButtonRecipe(s);
        useCardSelectors(s);
    }

    // Return tokens for external use
    return { colors, spacing, typography };
}

Naming Conventions

Follow these patterns for consistent, discoverable composables:

TypePatternExample
Variablesuse<Context>VariablesuseColorVariables, useSpacingVariables
Selectorsuse<Context>SelectorsuseButtonSelectors, useCardSelectors
Utilitiesuse<Context>UtilitiesuseSpacingUtilities, useLayoutUtilities
Recipesuse<Context>RecipeuseButtonRecipe, useInputRecipe
Themesuse<Context>ThemeuseDarkTheme, useBrandTheme

Best Practices

  • Always use default: true for variables in composables to ensure idempotency
  • Return typed references from variable composables so other code can use them
  • Provide sensible defaults for all configuration options
  • Call variable composables first before selectors/recipes that depend on them
  • Keep composables focused - one responsibility per composable
  • Document options with TypeScript interfaces for discoverability

FAQ