Composables
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:
- Idempotency — Safe to call multiple times without overwriting values
- Configurability — Accept options with sensible defaults
- Composition — Build complex systems from simpler building blocks
When to use composables
Use the Styleframe APIs directly when you define 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 are not redefined when composables are called multiple times - Return typed references — Functions return typed variable references that other code can safely use
@styleframe/theme package ships pre-built composables for colors, spacing, typography, borders, utilities, modifiers, and component recipes. You can use them as-is or write your own using the patterns below.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 redefines 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 does not already exist:
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(${spacing} * 0.5)`,
{ default: true },
);
const spacingMd = variable("spacing.md", ref(spacing), { default: true });
const spacingLg = variable(
"spacing.lg",
css`calc(${spacing} * 1.5)`,
{ default: true },
);
return { spacing, spacingSm, spacingMd, spacingLg };
}
import { styleframe } from "styleframe";
import { useSpacingVariables } from "./useSpacingVariables";
const s = styleframe();
const { ref, selector } = s;
// Safe to call from multiple places
const { spacingMd } = useSpacingVariables(s);
selector(".card", {
padding: ref(spacingMd),
});
export default s;
:root {
--spacing: 1rem;
--spacing--sm: calc(var(--spacing) * 0.5);
--spacing--md: var(--spacing);
--spacing--lg: calc(var(--spacing) * 1.5);
}
.card {
padding: var(--spacing--md);
}
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 Variables
Create variable composables that accept custom values:
import type { Styleframe } from "styleframe";
interface ColorOptions {
primary?: string;
secondary?: string;
accent?: string;
}
export function useColorVariables(
s: Styleframe,
options: ColorOptions = {},
) {
const { variable } = s;
const {
primary = "#006cff",
secondary = "#6c757d",
accent = "#f59e0b",
} = options;
const colorPrimary = variable("color.primary", primary, { default: true });
const colorSecondary = variable("color.secondary", secondary, { default: true });
const colorAccent = variable("color.accent", accent, { default: true });
return { colorPrimary, colorSecondary, colorAccent };
}
import { styleframe } from "styleframe";
import { useColorVariables } from "./useColorVariables";
const s = styleframe();
// Use defaults
useColorVariables(s);
// Or customize
useColorVariables(s, { primary: "#7c3aed", accent: "#ec4899" });
export default s;
:root {
--color--primary: #7c3aed;
--color--secondary: #6c757d;
--color--accent: #ec4899;
}
Configurable Selectors
Create selectors that can be customized per-project while working out of the box:
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",
},
});
}
import { styleframe } from "styleframe";
import { useCardSelectors } from "./useCardSelectors";
const s = styleframe();
// Use defaults
useCardSelectors(s);
// Or customize
useCardSelectors(s, { padding: "lg", borderRadius: "1rem" });
export default s;
Configurable Recipes
Create recipes that generate only the variants you need:
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;
const { colorPrimary, colorSecondary } = useColorVariables(s);
const colorMap: Record<string, object> = {};
const sizeMap: Record<string, object> = {};
// Build only the color variants requested
if (colors.includes("primary")) {
colorMap.primary = {
backgroundColor: ref(colorPrimary),
color: "white",
};
}
if (colors.includes("secondary")) {
colorMap.secondary = {
backgroundColor: ref(colorSecondary),
color: "white",
};
}
// Build only the size variants requested
if (sizes.includes("sm")) {
sizeMap.sm = { padding: "0.25rem 0.5rem", fontSize: "0.875rem" };
}
if (sizes.includes("md")) {
sizeMap.md = { padding: "0.5rem 1rem", fontSize: "1rem" };
}
if (sizes.includes("lg")) {
sizeMap.lg = { padding: "0.75rem 1.5rem", fontSize: "1.125rem" };
}
recipe({
name: "button",
base: {
borderRadius: "0.25rem",
border: "none",
cursor: "pointer",
fontWeight: "500",
},
variants: {
color: colorMap,
size: sizeMap,
},
defaultVariants: {
color: "primary",
size: "md",
},
});
}
import { styleframe } from "styleframe";
import { useButtonRecipe } from "./useButtonRecipe";
const s = styleframe();
// Generate all default variants
useButtonRecipe(s);
// Or generate only what you need
useButtonRecipe(s, {
colors: ["primary", "danger"],
sizes: ["md", "lg"],
});
export default s;
The Composition Pattern
The composition pattern builds complex design systems from simpler composables. A single entry point can orchestrate your entire design system.
import type { Styleframe } from "styleframe";
import { useColorVariables } from "./useColorVariables";
import { useSpacingVariables } from "./useSpacingVariables";
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);
// Components: Built on top of tokens
if (includeComponents) {
useButtonRecipe(s);
useCardSelectors(s);
}
// Return tokens for external use
return { colors, spacing };
}
import { styleframe } from "styleframe";
import { useDesignSystem } from "./useDesignSystem";
const s = styleframe();
const { selector, ref } = s;
// One line sets up your entire design system
const { colors, spacing } = useDesignSystem(s);
// You can still use the returned tokens directly
selector(".custom-element", {
color: ref(colors.colorPrimary),
margin: ref(spacing.spacingMd),
});
export default s;
Naming Conventions
Follow these patterns for consistent, discoverable composables:
| Type | Pattern | Example |
|---|---|---|
| Variables | use<Context>Variables | useColorVariables, useSpacingVariables |
| Selectors | use<Context>Selectors | useButtonSelectors, useCardSelectors |
| Utilities | use<Context>Utilities | useSpacingUtilities, useLayoutUtilities |
| Recipes | use<Context>Recipe | useButtonRecipe, useInputRecipe |
| Themes | use<Context>Theme | useDarkTheme, 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
{ default: true } before calling the composable, or use themes to override values in specific contexts.Styleframe instance as its first parameter, making it framework-agnostic.Output Format
Learn how Styleframe recipes generate CSS utility classes, understand the connection between recipe fields and utilities, and master the class naming conventions.
Imports
Learn the two ways to import Styleframe styles into your application—global imports for centralized design systems and per-file imports for component-scoped styling.