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

→ Chainable color transforms that resolve at build time into plain static strings.

Color Function

color() is a chainable helper for transforming colors — lighten, darken, adjust alpha, mix, rotate hue, and more. Everything runs at build time: the transformed value lands in the generated CSS as a plain static string, with no runtime cost and no color math shipped to the browser.

It's a helper like any other — a function you call to produce a value — and like every helper, it resolves while your styles compile. If you want the deeper why behind that build-time line, the Compiler concept is the read.

What it's for

The everyday use is deriving variations of a color. Give it one brand color and generate a whole family of shades — a lighter hover, a darker press, a muted disabled state, a semi-transparent overlay — instead of hand-picking each hex and hoping they stay in tune when the brand color changes. Store those derived shades as variables and the rest of your system reads clean token names while color() keeps the shades honest to their source.

One boundary matters enough to lead with, because it's the thing that trips people. color() can only transform values it can see at build time: raw colors (#0070f3, red, rgb(...)) and static token references like {colors.brand.primary}. A value that only exists at runtime — a responsive or conditional token, or anything wired through {props.X} — passes straight through unchanged, because there's no shade to compute from a value that doesn't exist yet when the compiler runs. So you derive shades from static atoms, not from themed molecules. For runtime-derived shades on a themed value, declare the shade directly under the conditional scope instead — the Theming page shows exactly that pattern.

Basic usage

Chain transforms onto a source color and hand the result to any color property:

/components/card.css.ts
import { styled } from "@salty-css/react/styled";
import { color } from "@salty-css/core/helpers";

export const Card = styled("div", {
  base: {
    backgroundColor: color("#000000").alpha(0.5),   // semi-transparent black
    borderColor: color("#0070f3").lighten(0.2),     // a lighter brand edge
    color: color("#ffffff").darken(0.1),            // slightly softened white
  },
});

Each call resolves at build time to a finished string — rgba(0, 0, 0, 0.5) and friends — baked into saltygen/index.css. There's no color() call left in your bundle.

Deriving a palette for your variables

This is the pattern the "make variations" use is really about: define one source color, derive the family from it, and expose the shades as tokens. Change the source and the whole family moves with it.

/styles/variables.css.ts
import { defineVariables } from "@salty-css/core/factories";
import { color } from "@salty-css/core/helpers";

const brand = "#0070f3";

export default defineVariables({
  colors: {
    brand: {
      main: brand,
      light: color(brand).lighten(0.2),
      lighter: color(brand).lighten(0.4),
      dark: color(brand).darken(0.2),
      darker: color(brand).darken(0.4),
      muted: color(brand).desaturate(0.3),
      overlay: color(brand).alpha(0.2),
    },
  },
});

Components reference the derived shades by path and never touch the math:

Example
export const Panel = styled("section", {
  base: {
    background: "{colors.brand.overlay}",
    borderColor: "{colors.brand.dark}",
  },
});

One source of truth for the color, a coherent palette derived from it, and a single edit to reshade the entire set.

Theming interactive states

The other frequent use is keeping hover/press/disabled states in tune with a base color, derived right where they're used:

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

export const Button = styled("button", {
  base: {
    backgroundColor: "{colors.brand.primary}",
    color: "#ffffff",
    transition: "background-color 0.2s ease",

    "&:hover":  { backgroundColor: color("{colors.brand.primary}").lighten(0.1) },
    "&:active": { backgroundColor: color("{colors.brand.primary}").darken(0.1) },
    "&:disabled": {
      backgroundColor: color("{colors.brand.primary}").desaturate(0.5).alpha(0.6),
    },
  },
});

Remember the boundary from above: {colors.brand.primary} works here because it's a static atom. If your button's base color were a themed molecule ({theme.buttonBg}), the derive wouldn't apply — you'd declare the hover and disabled shades under conditional per mode instead. The Theming page has that worked out in full.

Reference

Color sources

color() accepts hex, rgb()/rgba(), hsl()/hsla(), named CSS colors, and static token references:

Example
color("#ff0000");
color("#f00");
color("rgb(255, 0, 0)");
color("rgba(255, 0, 0, 0.5)");
color("hsl(210, 60%, 45%)").darken(0.1); // HSL input keeps lighten/darken predictable
color("steelblue");
color("{colors.brand.primary}");         // static token only

Methods

All methods are chainable and return a color() instance until you output a string.

Transparency

Example
color("#ff0000").alpha(0.5);                 // set absolute alpha (0–1)
color("#ff0000").fade(0.2);                  // reduce opacity by a relative amount
color("rgba(255, 0, 0, 0.5)").opaque();      // remove transparency

Lightness

Example
color("#ff0000").lighten(0.2);               // relative, 0–1
color("#ff0000").darken(0.2);                // relative, 0–1
color("#ff0000").lightness(0.8);             // absolute, 0–1

Saturation

Example
color("#ff0000").saturate(0.2);              // relative, 0–1
color("#ff0000").desaturate(0.2);            // relative, 0–1
color("#ff0000").grayscale();                // remove all saturation

Hue

Example
color("#ff0000").rotate(90);                 // rotate hue by degrees
color("#ff0000").hue(180);                   // set absolute hue, 0–360

Blend

Example
color("#ff0000").mix("#0000ff", 0.5);        // mix with another color (0–1)
color("#ff0000").negate();                   // complementary color

Output formats

The default output is rgb() / rgba(). Ask for a specific format at the end of the chain:

Example
color("rgb(255, 0, 0)").hex();               // "#ff0000"
color("#ff0000").rgb();                      // "rgb(255, 0, 0)"
color("#ff0000").alpha(0.5).rgba();          // "rgba(255, 0, 0, 0.5)"
color("#ff0000").hsl();                      // "hsl(0, 100%, 50%)"
color("#ff0000").alpha(0.5).hsla();          // "hsla(0, 100%, 50%, 0.5)"

Color spaces and invalid input

color() parses sRGB — the same space the browser uses for #rrggbb, rgb(), hsl(), and named colors — and computes transforms in HSL internally, which is why .lighten(), .darken(), and .saturate() operate on perceptual axes rather than raw channels. Wide-gamut input (P3, oklch) is not parsed today; if you need it, ship the wide-gamut value as a raw string gated behind @supports (color(display-p3 1 1 1)).

If color() can't parse the input — a missing token path, a malformed string — it throws at build time. With defineConfig({ strict: 'warn' }) you get a warning instead and the original value passes through. Either way it surfaces in your terminal; it never reaches the browser as a silent fallback.