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
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: trueoption 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:
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 };
}
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 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;
// 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",
},
});
}
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 lets you build complex design systems from simpler composables. A single entry point can orchestrate your entire design system.
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 };
}
import { styleframe } from "styleframe";
import { useDesignSystem } from "./useDesignSystem";
const s = styleframe();
// One line sets up your entire design system
const { colors, spacing } = useDesignSystem(s);
// You can still use the returned tokens directly
const { selector, ref } = s;
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: truefor 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.
Merging
Combine multiple Styleframe instances into a single unified configuration. Perfect for composing design systems, sharing configurations across projects, or building modular styling architectures.