Compose Reusable Style Patterns With defineTemplates — Share Compound Tokens Across Typed React Components
Reusable Style Templates for React
Templates are reusable style bundles. Where a variable holds a single value, a template holds a group of CSS properties — and can carry its own variants. Define a textStyle template once and apply it by key anywhere a style object is accepted; the compiler inlines the declarations at the call site.
Templates come in two forms: static (a plain nested object — each leaf is a path you apply by name) and function-based (a function that accepts a value at the call site and returns a style object). Both are defined with defineTemplates.
Creating Templates
You can define templates using the defineTemplates function:
// /styles/templates.css.ts import { defineTemplates } from "@salty-css/core/factories"; export default defineTemplates({ // Static templates for text styles textStyle: { headline: { small: { fontSize: "{fontSize.heading.small}", fontWeight: "600", lineHeight: "1.2", }, regular: { fontSize: "{fontSize.heading.regular}", fontWeight: "600", lineHeight: "1.2", }, large: { fontSize: "{fontSize.heading.large}", fontWeight: "700", lineHeight: "1.1", }, }, body: { small: { fontSize: "{fontSize.body.small}", lineHeight: "1.5", }, regular: { fontSize: "{fontSize.body.regular}", lineHeight: "1.4", }, }, }, // Dynamic function templates card: (padding: string) => { return { padding, borderRadius: "8px", boxShadow: "0 2px 10px rgba(0, 0, 0, 0.1)", backgroundColor: "#ffffff", }; }, });
Using Templates in Components
Templates are applied by using the template name as a property in your component styles:
import { styled } from "@salty-css/react/styled"; // Using static templates export const Heading = styled("h1", { base: { textStyle: "headline.large", // Apply the headline.large template }, }); export const Paragraph = styled("p", { base: { textStyle: "body.regular", // Apply the body.regular template }, }); // Using dynamic function templates export const CardComponent = styled("div", { base: { card: "2rem", // Pass "2rem" to the card template function }, variants: { compact: { true: { card: "1rem", // Pass "1rem" to the card template function }, }, }, });
Nesting Templates
Templates can be nested to create more complex reusable patterns:
export default defineTemplates({ surface: { primary: { backgroundColor: "{colors.background.primary}", color: "{colors.text.primary}", }, secondary: { backgroundColor: "{colors.background.secondary}", color: "{colors.text.secondary}", }, }, panel: (variant: "default" | "elevated") => { return { surface: "primary", // Apply the surface.primary template borderRadius: "8px", ...(variant === "elevated" ? { boxShadow: "0 4px 12px rgba(0, 0, 0, 0.1)", } : {}), }; }, });
A real-world text-style template
Here's the text-style template this very website uses. It shows how base + per-key styles compose into a reusable typography system, with the actual font-size values pulled from responsive tokens:
// /styles/text-styles.css.ts import { defineTemplates } from "@salty-css/core/factories"; export default defineTemplates({ textStyle: { headline: { base: { fontWeight: "300", letterSpacing: "0.0125em", lineHeight: "1.2em", fontFamily: "var(--font-family-logo)", }, small: { fontSize: "{fontSize.headline.small}" }, regular: { fontSize: "{fontSize.headline.regular}" }, large: { fontSize: "{fontSize.headline.large}" }, }, body: { base: { fontWeight: "300", letterSpacing: "0.0125em", lineHeight: "1.5em", }, xs: { fontSize: "{fontSize.body.xs}" }, small: { fontSize: "{fontSize.body.small}" }, regular: { fontSize: "{fontSize.body.regular}", lineHeight: "1.4em", }, large: { fontSize: "{fontSize.body.large}", lineHeight: "1.3em", }, }, code: { regular: { fontSize: "{fontSize.code.regular}", fontWeight: "300", letterSpacing: "0.025em", lineHeight: "1.66em", }, }, }, });
Then at call sites:
styled("h1", { base: { textStyle: "headline.large" } }); styled("p", { base: { textStyle: "body.regular" } }); styled("code", { base: { textStyle: "code.regular" } });
Function templates: dynamic parameters
Function templates accept a value at the call site and return a style object. The argument can be a scalar ("2rem" above) or any shape you find useful — an options object, a token name, anything:
// /styles/templates.css.ts import { defineTemplates } from "@salty-css/core/factories"; export default defineTemplates({ // Function templates accept any value at the call site and return a style object. card: (padding: string) => ({ padding, borderRadius: "8px", boxShadow: "0 2px 10px rgba(0, 0, 0, 0.1)", backgroundColor: "{theme.background}", }), // The argument can be a more structured object too. surface: ({ tone, elevated }: { tone: "muted" | "loud"; elevated?: boolean }) => ({ background: tone === "loud" ? "{colors.brand.main}" : "{theme.altBackground}", color: tone === "loud" ? "white" : "{theme.color}", boxShadow: elevated ? "0 4px 12px rgba(0,0,0,0.12)" : "none", }), });
Call sites pass the argument straight through:
import { styled } from "@salty-css/react/styled"; export const Card = styled("div", { base: { card: "2rem", surface: { tone: "muted", elevated: true }, }, });
Use function templates when the same pattern needs slightly different values each time (padding, color, an entire variant object) and you don't want to materialise every combination as a static template.
Runtime style injection with {props.X}
Inside a template (or any Salty style object), the parser recognises {props.X} and {-props.X} placeholders and rewrites them into CSS custom properties. Salty CSS reads matching values from the rendered component's props at runtime and sets the custom property — so a single static rule can take dynamic per-instance values without becoming a new variant.
// /styles/templates.css.ts import { defineTemplates } from "@salty-css/core/factories"; export default defineTemplates({ highlight: { base: { // The `{props.tint}` placeholder maps to a CSS variable on the element. background: "{props.tint}", color: "{props.fg}", }, }, });
import { styled } from "@salty-css/react/styled"; export const Pill = styled("span", { base: { highlight: true, // pull in the template padding: "2px 8px", borderRadius: "999px", }, });
<Pill tint="aqua" fg="black">New</Pill> <Pill tint="tomato" fg="white">Hot</Pill>
Use the dash form ({-props.X}) when the prop name should be dash-cased in the generated CSS variable.
{props.X}is a runtime escape hatch — every prop you reference becomes a tiny extra inline style. Prefer variants for closed sets of values; reach for{props.X}when the value is genuinely open-ended (a user-picked color, an animation duration computed at runtime, etc.).
Template variants
Template nodes can declare named variant bundles — the same ergonomic as styled({ variants }), but reusable across components. A node becomes "rich" the moment it has a base or variants key; otherwise the existing flat shape (above) keeps working untouched.
// /styles/templates.css.ts import { defineTemplates } from "@salty-css/core/factories"; export default defineTemplates({ textStyle: { heading: { base: { fontFamily: "{fonts.heading}", lineHeight: "1.2", }, variants: { weight: { light: { fontWeight: "300" }, regular: { fontWeight: "600" }, heavy: { fontWeight: "900" }, }, emphasis: { muted: { color: "{colors.text.muted}" }, loud: { color: "{colors.brand.primary}", textTransform: "uppercase" }, }, italic: { true: { fontStyle: "italic" }, }, }, defaultVariants: { weight: "regular" }, compoundVariants: [ { weight: "heavy", italic: true, css: { letterSpacing: "-0.005em" } }, ], // Descendants — declare only what's different about them. small: { base: { fontSize: "{fontSize.heading.small}" } }, large: { base: { fontSize: "{fontSize.heading.large}", lineHeight: "1.1" }, defaultVariants: { weight: "heavy" }, }, }, }, });
Call sites pick a path and (optionally) one variant value per axis. Two equivalent forms:
// String form — compact, ideal for one or two axes: styled("h1", { base: { textStyle: "heading.large@weight=light", }, }); styled("h2", { base: { textStyle: "heading.large@weight=heavy&emphasis=loud&italic", }, }); // Object form — best with multiple axes / boolean variants: styled("h1", { base: { textStyle: { name: "heading.large", weight: "heavy", emphasis: "loud", italic: true, }, }, }); // Parent ref — picks up `heading.base` only, no size leaf: styled("h2", { base: { textStyle: "heading" } });
A leaf inherits its parent's base, variants, defaultVariants, compoundVariants, and anyOfVariants. When a leaf re-declares an axis value (e.g., heading.large.variants.weight.heavy), it replaces the parent's bundle for that axis-value rather than merging — restate any properties you want to keep. The full resolution algorithm, edge cases, and design rationale live in docs/template-variants-spec.md.
When to use templates
Templates earn their keep for typography systems and other multi-property patterns that repeat across many components. Static templates suit closed systems where the full set of values is known ahead of time; function templates suit patterns that need a value passed in per use. Keep hierarchies reasonably flat — three levels is usually the limit before readability suffers. When an axis has two or more values that vary independently of the leaf (like a weight that applies to all heading sizes), declare it as a named variant on the parent node rather than duplicating it on every leaf.