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

→ Values decided in code — a variable, an import, a function, or a prop the consumer sets.

Dynamic Values

A value in a Salty style doesn't have to be a hardcoded literal. Anywhere you'd write background: "red", you can hand Salty a variable, a composed string, a value pulled in from another file, a function — even an async one — or a typed prop the consumer sets at the call site. Same styling API, more ways to decide what the value actually is.

The one rule underneath it, and the one that keeps expectations honest: almost all of this resolves at build time. A variable, an import, a function, a promise — Salty runs it once while compiling and bakes the result straight into the static stylesheet. The CSS you ship is identical to what you'd have typed by hand; it just got typed by your code instead of your fingers. The single exception is the css-* prop path in the last example, which deliberately stays dynamic all the way to the browser — that's the one you reach for when the value is genuinely the user's to pick. If you want the deeper why behind that build-time line, the Compiler concept is the read.

Example 1 — A value held in a variable

The smallest step past a literal. Instead of inlining the color, name it — then compose with it however JavaScript lets you.

components/wrapper.css.ts
import { styled } from "@salty-css/react/styled";

const brand = "#0070f3";

export const Wrapper = styled("div", {
  base: {
    background: brand,
    // it's just JS, so compose freely
    border: `2px solid ${brand}`,
  },
});

There's nothing runtime about this. brand is read while the file compiles, and the generated CSS contains the literal #0070f3 — the variable has already done its job and vanished by the time anything ships. This is mostly an authoring convenience: one place to change the color, and the full expressiveness of template strings for building values out of it. When a value is a real design token you want shared and typed across the whole system rather than a local convenience, that's Variables & Tokens, not a loose const — but for a one-file value, a const is exactly right.

Example 2 — A value that stays a CSS variable

Sometimes you don't want the value resolved away. You want to author a real CSS custom property with a sensible fallback, and leave a knob open for the browser to turn later.

How you define it

components/wrapper.css.ts
import { styled } from "@salty-css/react/styled";

const fallback = "red";

export const Wrapper = styled("div", {
  base: {
    background: `var(--bg, ${fallback})`,
  },
});

How you use it

Set --bg on a parent — here another styled component — and every Wrapper beneath it picks the value up through the cascade:

components/panel.css.ts
import { styled } from "@salty-css/react/styled";

export const Panel = styled("section", {
  base: {
    "--bg": "green",
  },
});
Example
import { Panel } from "./panel.css";
import { Wrapper } from "./wrapper.css";

<Panel>
  <Wrapper>…</Wrapper>
</Panel>

The interpolation (${fallback}) is still pure build-time — it bakes red into the rule as the default. But var(--bg, …) itself is left standing in the output, so a parent that sets --bg repaints the Wrapper beneath it — no new variant, and no change to Wrapper at all. This is the framework-native way to leave a per-instance knob open, and it's the bridge to the last example: the value is authored in TypeScript, but resolved in the browser.

Example 3 — A computed value, even an async one

A value can be a function. Salty calls it at build time and uses what comes back. And because it's build time, the function is allowed to be async — Salty will await a promise before writing the stylesheet. That means you can pull a value from an API — a color from a headless CMS, a token from a design-system endpoint — and have it end up in your CSS.

components/wrapper.css.ts
import { styled } from "@salty-css/react/styled";

const fallback = "red";

export const Wrapper = styled("div", {
  base: {
    background: async () => {
      const res = await fetch("https://salty-css.dev/api/tokens");
      const { color } = await res.json();
      return color ?? fallback;
    },
  },
});

The call site stays boringly normal — <Wrapper> takes no special prop; the async work already happened at build. Which is exactly the part it's worth being completely straight about, because the word async invites the wrong picture: this runs once, at build, and then it's frozen. The fetch fires while your site compiles, the resolved color is written into the static stylesheet as a plain literal, and every visitor gets that same baked value. It is not re-fetched per request, per render, or per user — there's no styling code left in the browser to re-run it.

So: ever wanted performant, DB-driven design tokens? Now's your chance — along with the chance to explain, every time design has a "silly" one-off request, that no, the update isn't live the instant they make it.

That's the shape of it: the right tool for reading a value at build (a CMS that art-directs your palette, an environment-specific token), and the wrong tool for anything that has to differ between visitors or change after the build. For live-per-request values, that's Runtime styles; for user-chosen values, keep reading to Example 5.

Example 4 — Sharing values across files

None of these values has to live in the same file as the component. Export a value from one module, import it into your .css.ts, and use it exactly as if it were local — handy for a shared palette or a mapping several components read from.

How you define it

theme/palette.ts
// a plain module, no Salty needed
export const palette = {
  brand: "#0070f3",
  danger: "#e5484d",
} as const;

How you use it

components/wrapper.css.ts
import { styled } from "@salty-css/react/styled";
import { palette } from "../theme/palette";

export const Wrapper = styled("div", {
  base: { background: palette.brand },
});

The one warning worth reading

Because your .css.ts files are evaluated in Node at build time, whatever they import is imported into your build. A small, plain module like the palette above is free — it's just data. But if that imported file pulls in a heavy third-party library, especially one that assumes a browser and touches window, you can slow compilation down noticeably (the import is parsed on every change) or crash the build outright. The fix is simple: keep .css.ts files style-focused, and if you need a value out of a heavy module, derive it once in a lightweight helper file and import the plain result — not the library.

To keep this in proportion, though: exporting a custom color mapping, or anything that's really just data, works fine and always will. And if some import adds a couple of seconds to a build but saves a lot of dev spaghetti, that's a fine tradeoff — a couple of seconds at build time never reaches your users, and treating it as unacceptable usually costs more in CI/CD complexity and developer headaches than it saves. Reach for the lightweight-helper split when a library genuinely breaks the build or drags it out, not as a reflex for every import.

Example 5 — Values the consumer sets

Everything so far is decided by you, the component author, at build time. But some values are genuinely the consumer's to pick — a color a user chose, a tint that comes in as a prop. For those, Salty gives you a typed prop token: reference {props.X} in your styles, and the compiler exposes a css-X prop on the component, wiring the value through a CSS variable for you.

How you define it

components/wrapper.css.ts
import { styled } from "@salty-css/react/styled";

export const Wrapper = styled("div", {
  base: {
    // fallback keeps it sane when the prop is unset
    background: "{props.bg}",
  },
});

How you use it

Example
import { Wrapper } from "./wrapper.css";

<Wrapper css-bg="green">…</Wrapper>

Under the hood, Salty writes css-bg to the element's inline style as --props-bg, and your compiled rule already reads it via var(--props-bg). A few things worth knowing: the token is camelCase ({props.bgColor}), the JSX prop is its dash-cased twin (css-bg-color), and the CSS variable is --props-bg-color. An unset prop writes nothing, so pair the token with a fallback — "{props.bg}" compiles against var(--props-bg, …) — when you want a default. And css-* props are stripped before forwarding, so they never leak onto the DOM as stray attributes.

This is the ergonomic version of Example 2. You could hand-roll it — declare var(--bg, …) yourself and ask consumers to set --bg through the style prop — and that's a perfectly good pattern when the variable name itself is the shared contract (theming variables several components read, or a value a parent wrapper sets). But when the component owns the contract and you want a typed, discoverable, autocompleted knob at the call site, css-* props are the pleasant path and style=undefined is the manual one. This is also the one mechanism on this page that stays live in the browser: the value rides a real CSS variable to runtime rather than being baked away. For the heavier per-request story — CMS-driven or tenant-specific styles resolved on the server — see Runtime styles.