Version 0.4.0 now released! See the release notes on GitHub Releases

→ Bundle the handful of properties you always set as a group under a single name.

Templates

Templates are the shortest way to reuse a set of styles that always travel together. And right away, worth heading off the usual first thought: this is not "make a card template, use it everywhere." A plain styled component already is your card, and it composes as an atom into bigger components without any new machinery — that pattern is covered, no template needed. What templates are actually for is the smaller, fiddlier job — the handful of properties you always set as a group and would rather not get subtly wrong every single time you type them out.

The mental model I'd start from is the CSS shorthand. font and border each set several properties in one go — you write one declaration instead of five, and the browser expands it. A Salty template has the same intent: bundle several property–value pairs under one name and apply them together. The syntax is different and it's authored in TypeScript, but the payoff lands in the same place — you say the name, you get the whole group, and it compiles down to those same pairs (or a reusable class name) with none of the repetition and none of the drift.

The upside is fewer mistakes: with templates you're not fat-fingering the same styles over and over. Nailing down that one font combo shouldn't feel like a round of Tekken — more of a kids' holiday crossword, the kind that earns you a "good job" from your designer dad.

Everything below follows one shape: define the template once, then apply it by using its name as a key inside a component's base. Templates are defined with defineTemplates, and — like everything in Salty — compiled away at build time, so nothing about this reaches the browser as runtime work.

/styles/templates.css.ts
import { defineTemplates } from "@salty-css/core/factories";

export default defineTemplates({
  // templates go here
});

One precision note before the examples: templates resolve in their own templates cascade layer, which sits below your component styles. So a template gives you the baseline group, and anything you set directly on the component still wins — you're never fighting a template to override one property of it.

Examples

Text styles — the one I reach for most

This is the case templates were made for — the one I love them for, and the one I always use them for. Credit where it's due: it's lifted straight from Panda CSS's text styles, which nailed the pattern. A font shorthand is great, but the moment you're implementing real designs, a designer will want fine-tuning that the plain shorthand just doesn't carry — a specific line-height here, a touch of letter-spacing there. A text-style template lets you fold family, size, weight, and whatever else into one name like headline.large, tune each one exactly, and stop copy-pasting the same handful of declarations onto every heading.

Define it — a base shared across each group, then per-size leaves that only say what's different:

/styles/text-styles.css.ts
import { defineTemplates } from "@salty-css/core/factories";

export default defineTemplates({
  textStyle: {
    headline: {
      base: {
        fontFamily: "{fonts.headline}",
        fontWeight: "300",
        letterSpacing: "0.0125em",
        lineHeight: "1.2em",
      },
      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: {
      base: {
        fontFamily: "{fonts.code}",
        fontSize: "{fontSize.code.regular}",
      },
      regular: {
        fontWeight: "300",
        letterSpacing: "0.025em",
        lineHeight: "1.66em",
      },
    },
  },
});

Then use it — the template name is a key, its value is the path you want:

Example
import { styled } from "@salty-css/react/styled";

export const Title = styled("h1", { base: { textStyle: "headline.large" } });
export const Copy = styled("p", { base: { textStyle: "body.regular" } });
export const Code = styled("code", { base: { textStyle: "code.regular" } });

That's the whole system, and it's the version I'd actually ship. Templates can also carry variants — the same machinery styled has — for the small additions a design inevitably asks for. The classic one: a bolder cut of a headline with a touch more tracking. That isn't a second template, it's one variant on the group you already have. Keep it light; a text style rarely wants more than an axis or two.

Example
// headline, now with an opt-in bold cut (body and code unchanged)
export default defineTemplates({
  textStyle: {
    headline: {
      base: {
        fontFamily: "{fonts.headline}",
        fontWeight: "300",
        letterSpacing: "0.0125em",
        lineHeight: "1.2em",
      },
      variants: {
        bold: {
          true: { fontWeight: "600", letterSpacing: "0.02em" },
        },
      },
      small: {
        fontSize: "{fontSize.headline.small}",
      },
      regular: {
        fontSize: "{fontSize.headline.regular}",
      },
      large: {
        fontSize: "{fontSize.headline.large}",
      },
    },
    // …body and code exactly as before
  },
});

There's deliberately no defaultVariants, so leaving the variant off gives you the base and nothing more — a plain headline.large is the light weight. Opt into the bolder cut only where you want it:

Example
// no variant → base only (weight 300)
export const Title = styled("h1", { base: { textStyle: "headline.large" } });

// opt in — string form, and a boolean variant is just a bare flag
export const Hero  = styled("h1", { base: { textStyle: "headline.large@bold" } });

// object form does the same, if you find it clearer
export const Hero2 = styled("h1", { base: { textStyle: { name: "headline.large", bold: true } } });

Two things worth knowing. A leaf inherits its parent's base and variants, so headline.large already carries the family, tracking, and the bold option — the leaf only adds its size. And here's the real payoff: those {fontSize.*} tokens can themselves be responsive tokens, so if your type scale is defined responsively, every text style scales across breakpoints with no media query in the template and no change at any call site. The names stay put; the values underneath them move.

For the next level up — driving a scale from values you can't know at build time, like a CMS-authored size set or a per-tenant font — a dedicated recipe will cover that rather than crowd it in here.

Interaction states

The other everyday case: the little cluster of properties that make something feel interactive — a cursor, a transition, a hover nudge, a visible focus ring. Easy to set inconsistently across a dozen components, which is exactly the kind of drift a template kills.

Define it once, as a base-only template:

/styles/templates.css.ts
import { defineTemplates } from "@salty-css/core/factories";

export default defineTemplates({
  interactive: {
    base: {
      cursor: "pointer",
      transition: "transform 150ms ease, background 150ms ease",
      "&:hover": { transform: "translateY(-1px)" },
      "&:focus-visible": { outline: "2px solid {theme.focusRing}", outlineOffset: "2px" },
    },
  },
});

A base-only template is pulled in with true:

Example
export const Button = styled("button", {
  base: {
    interactive: true,
    padding: "0.6em 1.2em",
    borderRadius: "6px",
  },
});

A border-ish shorthand

Real CSS shorthands are the inspiration, so here's a template that behaves like one — bundling width, style, color, and radius into a couple of named presets:

/styles/templates.css.ts
import { defineTemplates } from "@salty-css/core/factories";

export default defineTemplates({
  bordered: {
    subtle: { borderWidth: "1px", borderStyle: "solid", borderColor: "{theme.line}", borderRadius: "8px" },
    strong: { borderWidth: "2px", borderStyle: "solid", borderColor: "{theme.text}", borderRadius: "8px" },
  },
});
Example
export const Panel = styled("section", { base: { bordered: "subtle", padding: "1rem" } });

One naming note, and it matters: I called it bordered, not border. border is a real CSS shorthand already, so a template by that name would shadow it. Salty will let you do it — overriding a real property name is technically possible — but I wouldn't; you lose the plain shorthand and invite confusion for anyone reading the styles later. Give the template its own name and keep both.

Theming values

Templates and theming pair up well. A template can reference your themed tokens (the {theme.*} molecules), so the themed background-and-text pairing becomes one name — themed — that any component can pull in, and it flips with the scheme for free because the tokens do the flipping.

/styles/templates.css.ts
import { defineTemplates } from "@salty-css/core/factories";

export default defineTemplates({
  themed: {
    base: { background: "{theme.bg}", color: "{theme.text}" },
  },
});
Example
export const Card = styled("article", { base: { themed: true, padding: "1.5rem" } });

The one thing to keep straight is which tokens you reach for — a template should reference the themed molecule ({theme.bg}), not a raw brand color, or you freeze it to one scheme. That's a theming habit rather than a template one; the theming page covers it properly.

Function templates

Everything above applies a template by name. There's a second form that's easy to miss and quietly powerful: instead of a fixed bundle, you export a function that takes an argument at the call site and returns the style object. Reach for it when the pattern is identical every time but a value isn't — and because the function is plain TypeScript running at build time, the "work out the styles from the argument" part can be as clever as you like, with none of that logic shipping to the browser. What lands in the CSS is the resolved result.

The simplest form takes a single value:

/styles/templates.css.ts
import { defineTemplates } from "@salty-css/core/factories";

export default defineTemplates({
  // a vertical stack with a caller-chosen gap
  stack: (gap: string) => ({
    display: "flex",
    flexDirection: "column",
    gap,
  }),
});
Example
export const Column = styled("div", { base: { stack: "1rem" } });

The argument can be any shape, though — and an options object is where these earn their keep, because you can branch on it in real TypeScript:

Example
export default defineTemplates({
  surface: ({ tone, elevated }: { tone: "muted" | "loud"; elevated?: boolean }) => ({
    background: tone === "loud" ? "{theme.accent}" : "{theme.bgAlt}",
    boxShadow: elevated ? "0 4px 12px rgba(0, 0, 0, 0.12)" : "none",
  }),
});
Example
export const Callout = styled("aside", {
  base: { surface: { tone: "loud", elevated: true }, padding: "1.25rem", borderRadius: "10px" },
});

Type the parameter and that type flows straight to the call site — tone autocompletes, and a typo is a compile error rather than a silent miss. The rule of thumb between the two forms: a function template when the value is genuinely per-use and open-ended; a variant when it's a small closed set (three headline weights, say). They aren't rivals — they're the right shape for different jobs.

Beyond the basics

One more shape, worth knowing exists even if you don't need it today. {props.X} injection lets a template read a value straight off the rendered component's props at runtime, for the genuinely open-ended cases — a user-picked color, a duration you can't know ahead of time — where neither a variant nor a build-time function argument fits. It's the one template feature with a (tiny) runtime cost, and it's covered in full in the templates reference.

Coming soon — the recipes below put templates to work in real projects.