# Salty CSS > Build-time CSS-in-TS for React, Next.js and Astro. Compiles `styled(...)` calls > in `*.css.ts` files to plain CSS at build time — zero runtime cost, with full > TypeScript autocomplete on design tokens, themes, variants and media queries. > Works with React Server Components. This file contains the full rendered documentation for Salty CSS, concatenated for LLM ingestion. Three framework variants (React, Next.js, Astro) are included in order — they are translations of the same docs with framework-specific install commands, imports, and code snippets. Source: https://salty-css.dev/llms-full.txt — Generated: 2026-06-23 Companion index: https://salty-css.dev/llms.txt --- # Documentation — React Source: https://salty-css.dev/docs/react/ Salty CSS is a build-time CSS-in-**TS** library for React, Next.js and Astro. You author styles in `.css.ts` files, the compiler turns them into real CSS, and your runtime ships with no styling engine attached. Meow. It's a good fit when you want **typed styles, real design tokens, and theming that doesn't need a React provider** — without giving up on the developer experience of writing CSS next to your component. ### What you get - **Typed styles** — `styled("button", { ... })` returns a typed component; variants become typed props. - **Design tokens** — `defineVariables` for static, **responsive**, and **conditional** tokens (the conditional ones are how theming works). - **Theming without a provider** — flip a `data-theme` attribute on `` and every consumer of `{theme.color}` updates. - **Templates** — `defineTemplates` for reusable style bundles with their own variants. - **Media queries that read like English** — `media.minWidth(720).and.dark`. - **Fluid sizing** — `defineViewportClamp` for `clamp()`-based values that scale with the viewport. - **Type-safe class names** — `className({ ... })` when you want the styles without the component wrapper. - **A CLI** — `npx salty-css init` / `generate` / `build` / `up` for the boring bits. ### TL;DR — install it Inside any React, Next.js or Astro project: ```bash npx salty-css init ``` Then create a component in a `*.css.ts` file: ```ts // /components/my-button.css.ts import { styled } from "@salty-css/react/styled"; export const Button = styled("button", { base: { padding: "0.75rem 1.25rem", borderRadius: "6px", background: "{theme.color}", color: "{theme.background}", }, }); ``` …and use it like any other . ### Where to go next - **New here?** → [Quick Start](/docs/quick-start/) walks you from `init` to a themed component with variants in about 15 minutes. - **Adding it to a project?** → [Installation](/docs/installation/) covers the per-framework wiring. - **Need the reference?** → [`styled`](/docs/api/styled/), [`className`](/docs/api/classname/), [`defineConfig`](/docs/api/config/). ### Search the docs Hit `Ctrl + K` (or `⌘ + K` on macOS) anywhere in the docs to open the search modal — or click **Search** at the top of the sidebar. Matching is fuzzy: partial terms like `variant`, `media` or `clamp` are usually enough. ### What's in the docs #### Getting Started - [Quick Start](/docs/quick-start/) — install, your first component, variants, theming, fonts. - [Installation](/docs/installation/) — per-framework setup (Next.js, Vite, Webpack, Astro). - [Usage](/docs/usage/) — file suffixes, dev-time naming, DevTools. - [Troubleshooting](/docs/troubleshooting/) — the fix list for the most common issues. - [CLI](/docs/cli/) — use Salty CSS from the command line. - [FAQ](/docs/faq/) — short answers to the things people ask first. #### Styling - [Component styles](/docs/basics/) — `styled`, `className`, and where each one fits. - [Variables](/docs/variables/) — design tokens with `defineVariables` (static, responsive, conditional). - [Theming](/docs/theming/) — dark mode and multi-theme without a provider. - [Fonts](/docs/fonts/) — `defineFont` for local files and remote stylesheets. - [Imports](/docs/imports/) — pull in external CSS with `defineImport`. - [Class styles](/docs/classnames/) — `className` for plain class-based styles. - [Variants](/docs/variants/) — prop-driven variants, compound, and `anyOfVariants`. - [Overrides](/docs/overrides/) — extend components, swap elements, forward props with `passProps`. - [Media queries](/docs/media-queries/) — media queries, container queries, breakpoints. - [Animations](/docs/animations/) — keyframes, stagger, pause/resume. - [Templates](/docs/templates/) — reusable styles with `defineTemplates`. #### Utilities - [Viewport clamp](/docs/viewport-clamp/) — fluid responsive sizing. - [Color function](/docs/color-function/) — manipulate colors at build time. - [Modifiers](/docs/modifiers/) — custom value transformers. #### API reference - [`styled` function](/docs/api/styled/) — full API for the `styled` component factory. - [`className` function](/docs/api/classname/) — full API for class-based styles. - [`defineConfig`](/docs/api/config/) — every option that lives in `salty.config.ts`. - [`define*` factories index](/docs/api/define-factories/) — one-page index of every factory. ### Get support Join the [Salty CSS Discord server](https://discord.gg/R6kr4KxMhP) for help, or check the [GitHub repository](https://github.com/margarita-form/salty-css) for source and issues. --- # Getting Started — React Source: https://salty-css.dev/docs/react/getting-started/ Everything you need to get a Salty CSS project off the ground. Start with the **Quick Start** for a fifteen-minute end-to-end walkthrough, or jump into **Installation** if you'd rather wire the build plugin into an existing app. The remaining pages cover day-to-day **Usage**, the **CLI**, **ESLint** support, common **Troubleshooting**, and **FAQ** answers. --- # Quick Start — React Source: https://salty-css.dev/docs/react/quick-start/ Goal: by the end of this page you have Salty CSS installed, a typed component on screen, prop-driven variants, dark mode that flips without a provider, and a custom font registered. Budget about 15 minutes. ### 1. Install Inside any React, Next.js, Vite, or Astro project, run: ```bash npx salty-css init ``` The CLI detects your framework, adds the right plugin (`@salty-css/next`, `@salty-css/vite`, `@salty-css/webpack`, or `@salty-css/astro`), drops a `salty.config.ts` in the project root, and wires up the build. If you'd rather wire it up yourself, see [Installation](/docs/installation/). ### 2. Your first component All Salty definitions live in files matching `*.css.ts`, `*.css.tsx`, `*.salty.ts`, `*.styled.ts`, or `*.styles.ts` — the suffix is how the compiler picks them up at build time. ```ts // /components/card.css.ts import { styled } from "@salty-css/react/styled"; export const Card = styled("section", { base: { padding: "1.5rem", borderRadius: "12px", background: "{theme.background}", color: "{theme.color}", boxShadow: "0 1px 2px rgba(0, 0, 0, 0.06)", }, }); ``` Use it like any other : ```tsx import { Component } from "./my-component.css"; const MyPage = () => { return ( This is a Salty CSS component ); }; export default MyPage; ``` Note the `{theme.background}` / `{theme.color}` tokens — we'll define those in step 4. ### 3. Add variants Variants turn props into typed style branches. Add a `size` axis and a `tone` axis: ```ts // /components/card.css.ts import { styled } from "@salty-css/react/styled"; export const Card = styled("section", { defaultVariants: { size: "medium", tone: "neutral" }, base: { padding: "1.5rem", borderRadius: "12px", background: "{theme.background}", color: "{theme.color}", }, variants: { size: { small: { padding: "1rem", borderRadius: "8px" }, medium: { padding: "1.5rem" }, large: { padding: "2.5rem", borderRadius: "16px" }, }, tone: { neutral: {}, brand: { background: "{theme.highlight}", color: "{theme.background}" }, muted: { background: "{theme.altBackground}" }, }, }, compoundVariants: [ { size: "large", tone: "brand", css: { fontWeight: 600 } }, ], }); ``` `size` and `tone` are now typed props. Render with: ```tsx import { Button } from "./button/button.css"; export const MyComponent = () => { return (
); }; ``` For more on variants — including `anyOfVariants` for "any of these branches matches" rules — see [Variants](/docs/variants/). ### 4. Add design tokens and dark mode Salty themes are CSS variables under the hood. You declare two (or more) value sets under `conditional.theme`, and the active set is picked by an attribute on an ancestor — usually ``. No provider, no context. ```ts // /styles/themes.css.ts import { defineVariables } from "@salty-css/core/factories"; export const palette = defineVariables({ colors: { ink: "#0d1117", paper: "#ffffff", sand: "#f5f1e8", coral: "#ff6b5a", }, }); export const themes = defineVariables({ conditional: { theme: { light: { background: "{colors.paper}", altBackground: "{colors.sand}", color: "{colors.ink}", highlight: "{colors.coral}", }, dark: { background: "{colors.ink}", altBackground: "#1c2128", color: "{colors.paper}", highlight: "{colors.coral}", }, }, }, }); ``` Activate a theme by setting the attribute on any ancestor — usually the root: ```html ... ``` That's the whole switch. Every `{theme.background}` / `{theme.color}` reference in your styles now reads the dark values. Flip the attribute back to `light` and it all updates again. For a working toggle that persists the choice in `localStorage` and avoids the first-paint flash: ```tsx // /components/theme-toggle.tsx "use client"; import { useEffect, useState } from "react"; type Theme = "dark" | "light"; const STORAGE_KEY = "theme"; const readInitial = (): Theme => { if (typeof window === "undefined") return "light"; const saved = window.localStorage.getItem(STORAGE_KEY) as Theme | null; if (saved === "dark" || saved === "light") return saved; return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; }; export const ThemeToggle = () => { const [theme, setTheme] = useState("light"); useEffect(() => { const initial = readInitial(); setTheme(initial); document.documentElement.dataset.theme = initial; }, []); const toggle = () => { const next: Theme = theme === "dark" ? "light" : "dark"; setTheme(next); document.documentElement.dataset.theme = next; window.localStorage.setItem(STORAGE_KEY, next); }; return ( ); }; ``` To avoid a flash of the wrong theme on first paint in Next.js, inline a tiny pre-hydration script in `app/layout.tsx` (or `_document.tsx` for the Pages Router) so the attribute is set before React hydrates: ```tsx // /app/layout.tsx const setThemeScript = ` try { var t = localStorage.getItem("theme"); if (t !== "dark" && t !== "light") { t = matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; } document.documentElement.dataset.theme = t; } catch (e) {} `; export default function RootLayout({ children }) { return ( ``` The `is:inline` directive ensures the script runs before the first paint, so users never see a flash of the wrong theme. See [Theming](/docs/theming/) for the deeper guide (system preference, multiple independent groups, deriving shades with `color()`). ### 5. Register a custom font `defineFont` registers a font and gives you back something you can use as a `font-family` value, a CSS variable, a class name, or a `style` spread. ```ts // /styles/fonts.css.ts import { defineFont } from "@salty-css/core/factories"; export const display = defineFont({ name: "Mona Sans", fallback: "system-ui, -apple-system, sans-serif", variants: [ { src: "/fonts/Mona-Sans.woff2", weight: "200 900", style: "normal", display: "swap", }, ], }); ``` Use it in a styled component: ```ts import { styled } from "@salty-css/astro/styled"; import { display } from "../styles/fonts.css"; export const Title = styled("h1", { base: { fontFamily: display, fontSize: "clamp(2rem, 4vw, 3.5rem)", color: "{theme.color}", }, }); ``` `display` stringifies to its `font-family` value, so it works inline. You can also read `display.variable` (the CSS custom property), `display.className`, or `display.style` (an object you can spread onto a `style` prop). See [Fonts](/docs/fonts/) for remote fonts (`import`) and per-variant overrides. ### What you have now A typed component with variants, a theme that flips on a single attribute, and a custom font registered through `defineFont`. That's the everyday Salty CSS surface. ### Where to go next - **Variants in depth** → [Variants](/docs/variants/) (compound, `anyOfVariants`, defaults). - **Reusable style bundles** → [Templates](/docs/templates/). - **Fluid type / spacing** → [Viewport clamp](/docs/viewport-clamp/). - **Media queries and breakpoints** → [Media queries](/docs/media-queries/). - **Full reference** → [`styled`](/docs/api/styled/), [`className`](/docs/api/classname/), [`defineConfig`](/docs/api/config/). - **Per-request styles (CMS, tenant overrides)** → [`defineRuntime`](/docs/api/runtime/). ### Use the CLI - Scaffold a component: `npx salty-css generate [filePath]` - Force a CSS build: `npx salty-css build [directory]` - Bump packages: `npx salty-css up` ### Good to know 1. All Salty CSS definitions (`styled`, `className`, `keyframes`, `defineFont`, etc.) must live in `*.css.ts` / `*.css.tsx` (or `.salty.ts`, `.styled.ts`, `.styles.ts`). The suffix is the contract — that's how the compiler knows to evaluate the file. 2. `styled` can extend non-Salty components (`styled(ThirdPartyLink, { ... })`), but the wrapped component must accept a `className` prop. See [Overrides](/docs/overrides/). 3. CSS-in-JS values can be `string`, `number`, **function**, or **promise** — but importing heavy runtime libraries into a `*.css.ts` file will slow your build (and may crash if the library assumes a browser). Keep these files style-focused. ### If something didn't work → [Troubleshooting](/docs/troubleshooting/). The most common cause (a wrong filename suffix) is the very first entry. ### Get support Stuck? Drop into the [Salty CSS Discord](https://discord.gg/R6kr4KxMhP) and someone will untangle it with you. --- # Installation — Astro Source: https://salty-css.dev/docs/astro/installation/ The fastest way to start in any framework: ```bash npx salty-css init ``` The init command detects your framework, installs the right packages, creates `salty.config.ts`, and wires the build plugin into your existing bundler config. For most projects that's all you need. For a manual install on **Astro**, run: ```bash npm i @salty-css/astro ``` ### Astro 1. In your existing Astro repository you can run `npx salty-css init` to automatically configure Salty CSS. 2. Create your first Salty CSS component with `npx salty-css generate [filePath]` (e.g. `src/custom-wrapper`). 3. Import your component in any `.astro` page or layout and see it working! #### Manual configuration 1. Install the Astro integration: ```bash npm i @salty-css/astro ``` 2. Create `salty.config.ts` in your project root. 3. Add the Salty CSS integration to `astro.config.mjs`: ```ts import { defineConfig } from "astro/config"; import saltyIntegration from "@salty-css/astro/integration"; export default defineConfig({ integrations: [saltyIntegration()], }); ``` `saltyIntegration` accepts `{ srcDir?: string; rootDir?: string }`. `srcDir` defaults to `'src'`; `rootDir` defaults to the Astro config root. 4. Make sure that `salty.config.ts` and `astro.config.mjs` are in the same folder. 5. Build the `saltygen` directory by running your app once or via the CLI: `npx salty-css build [directory]`. 6. Import global styles from `saltygen/index.css` in your global stylesheet: `@import 'insert_path_to_index_css';`. ### Manual setup If `salty-css init` picks the wrong framework, can't find your bundler config, or you'd rather wire things up by hand, follow the manual setup for your stack. The framework-specific snippet above includes the precise commands and config edits; the steps below are the universal shape. 1. **Install the packages.** Each framework needs its runtime plus the core compiler: - Next.js: `npm i @salty-css/next @salty-css/core @salty-css/react` - React + Vite: `npm i @salty-css/vite @salty-css/core @salty-css/react` - React + Webpack: `npm i @salty-css/webpack @salty-css/core @salty-css/react` - Astro: `npm i @salty-css/astro @salty-css/core` 2. **Wire the bundler plugin.** `withSaltyCss(nextConfig)` for Next.js, `saltyPlugin(__dirname)` for Vite, `saltyPlugin(config, __dirname)` in `webpack.config.js` for Webpack, the integration for Astro. 3. **Create `salty.config.ts`** in the same directory as your bundler config (e.g. next to `next.config.ts` or `vite.config.ts`): ```ts import { defineConfig } from "@salty-css/astro/config"; export const config = defineConfig({ // Add variables, templates, modifiers as you grow. }); ``` 4. **Import the generated stylesheet.** With the default `importStrategy: 'root'`, Salty CSS expects one stylesheet to be imported at your app root. Most framework plugins do this for you on first run; if not, add `@import "../saltygen/index.css";` (or the appropriate path) to your global CSS. 5. **Build once.** Run your dev server (or `npx salty-css build`) so `saltygen/` exists before the first render. ### Peer dependencies You'll need: | Package | Version | Notes | | ------------ | --------------------------------------------- | -------------------------------------------------------------------------- | | `node` | 18 or newer | Required for the build pipeline (esbuild + ESM). | | `typescript` | 5.x | Needed because `.css.ts` files are evaluated through TypeScript. | | `react` | 18 or 19 (when using React/Next) | The `@salty-css/react` runtime targets modern React. | | `next` | 13 (App Router) or newer; tested through 16.2 | For `@salty-css/next`. Webpack and Turbopack both work via `withSaltyCss`. | | `vite` | 5 or newer | For `@salty-css/vite`. | | `astro` | 4 or newer | For `@salty-css/astro`. | The exact ranges live in each package's `peerDependencies` — check `package.json` if you're on a fringe version. ### Verify your install After running the dev server (or `npx salty-css build`), confirm: 1. **`saltygen/` exists** at the root of the package you initialised. It should contain at least `index.css` and a `salty.config.js` snapshot. 2. **`saltygen/index.css` is non-empty.** A few `@layer` declarations and your reset should be there even before you write any components. 3. **A test component renders styled.** Create a `*.css.ts` file with a tiny styled component, use it on a page, and inspect the element in DevTools — you should see a class like `s_xxxx` and a matching rule in the Styles panel. 4. **No build warnings about missing plugin.** Salty CSS logs a warning at build time if the plugin didn't load — search your terminal output for `salty-css`. If any step fails, jump to [Troubleshooting](/docs/troubleshooting/). The compiler can't warn you about two common structural mistakes: unexported `styled` calls, and `variants` accidentally nested inside `base` instead of alongside it. The [ESLint plugin](/docs/eslint/) catches both — worth setting up once. --- # Usage — Astro Source: https://salty-css.dev/docs/astro/usage/ ### Create components Salty CSS only picks up files whose names end with one of these suffixes: | Suffix | When to use it | | ------------ | ------------------------------------------------------------------ | | `.css.ts` | Default for any style or component file. Works everywhere. | | `.css.tsx` | Same as `.css.ts`, but JSX is allowed in the file. | | `.salty.ts` | Alias of `.css.ts` — pick whichever reads better in your project. | | `.styled.ts` | Alias of `.css.ts`, conventionally used for `styled` factories. | | `.styles.ts` | Alias of `.css.ts`, conventionally used for `defineTemplates` etc. | A `.ts` file with the same content but missing the right suffix will type-check fine but produce **no CSS** at build time — this is the single most common "my styles aren't appearing" cause. See [Troubleshooting](/docs/troubleshooting/) if you hit it. ### Component structure ```ts // /components/my-component.css.ts import { styled } from "@salty-css/astro/styled"; export const Component = styled("div", { className: "wrapper", // Optional custom class name element: "section", // Optional override for the rendered HTML element base: { // Base styles that are always applied display: "flex", padding: "1rem", backgroundColor: "#f5f5f5", }, variants: { // Conditional styles based on props size: { small: { padding: "0.5rem" }, large: { padding: "2rem" }, }, color: { primary: { backgroundColor: "blue", color: "white" }, secondary: { backgroundColor: "gray", color: "black" }, }, }, compoundVariants: [ // Styles applied when multiple variant conditions are met { size: "small", color: "primary", css: { borderRadius: "4px" }, }, ], }); ``` ### Using components ```astro --- // src/pages/index.astro import { Component } from "../components/my-component.css"; --- This is a Salty CSS component ``` ### Naming components in DevTools In development builds, every styled component renders with a `data-component-name` attribute matching its export name. Search for `[data-component-name="Button"]` in the elements panel to jump straight to it. You can override the label with the `displayName` option on the styled definition: ```ts export const PrimaryButton = styled("button", { displayName: "PrimaryButton", base: { /* … */ }, }); ``` In production builds the attribute is stripped, so it's a debugging aid only. ### Where to go next - **Add prop-driven styles** → [Variants](/docs/variants/) (and [`anyOfVariants`](/docs/variants/#anyof-variants---or-logic) for OR-logic). - **Share style bundles across components** → [Templates](/docs/templates/). - **Add design tokens** → [Variables](/docs/variables/). - **Add dark mode** → [Theming](/docs/theming/). - **Extend a third-party component** → [Overrides](/docs/overrides/). - **API reference** → [`styled`](/docs/api/styled/) · [`className`](/docs/api/classname/) · [`defineConfig`](/docs/api/config/). ### Demo Projects - **Next.js Demo Project**: [View on GitHub](https://github.com/margarita-form/salty-css-website) - **React + Vite Demo**: [View on GitHub](https://github.com/margarita-form/salty-css-react-vite-demo) - **CodeSandbox Demo**: [![Edit margarita-form/salty-css-react-vite-demo/main](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/p/github/margarita-form/salty-css-react-vite-demo/main?import=true&embed=1) --- # Troubleshooting — Astro Source: https://salty-css.dev/docs/astro/troubleshooting/ A short, opinionated checklist for the issues that come up most often. If your symptom isn't here, the [Discord server](https://discord.gg/R6kr4KxMhP) is the fastest way to get unstuck. ### My styles aren't appearing Work through these in order — the first one is the cause about 80% of the time. 1. **Wrong filename suffix.** Salty CSS only picks up `*.css.ts`, `*.css.tsx`, `*.salty.ts`, `*.styled.ts`, or `*.styles.ts`. A file named `my-component.ts` will type-check fine but produce no CSS. Rename it. 2. **`saltygen/index.css` is stale or missing.** Restart the dev server. If you're not using a dev server, run `npx salty-css build` to regenerate it. 3. **Plugin not wired up.** Confirm `withSaltyCss` (Next.js), `saltyPlugin` (Vite), or the Astro integration appears in your bundler config. Without the plugin, the compiler never runs. 4. **Importing `saltygen/index.css` is missed.** With `importStrategy: 'root'` (the default), the generated stylesheet must be imported once at the root of your app. Frameworks like Next.js do this for you; check the install snippet for your stack if you set things up manually. 5. **The component isn't actually used.** Salty CSS tree-shakes unused exports — if no module imports the styled component, no CSS is emitted for it. Make sure the export reaches a rendered tree. ### I can see the class but the rule isn't applying - **Specificity conflict.** A higher-specificity rule from another stylesheet may be winning. Inspect the element in DevTools and look at the cascade. Bump the Salty rule's `priority` (see [`styled` API](/docs/api/styled/)) or scope your override more tightly. - **Order of cascade layers.** Salty CSS uses `@layer` internally. Anything you import with [`defineImport`](/docs/imports/) lives in the earliest layer, so it always loses to your own styles — and vice versa, your imports won't override Salty rules. - **`anyOfVariants` losing to a regular variant.** `anyOfVariants` uses `:where()` and has zero specificity by design. If you need it to win, move the rule into a regular variant or a `compoundVariants` entry. ### A token reference shows up literally as `{colors.brand}` in the browser That's the parser refusing to resolve an unknown path: - Typo? Check `defineVariables({ colors: { ... } })` matches the path exactly. - Did you forget to register the variables file? With `defineVariables` in a `*.css.ts` file, the file just needs to be imported somewhere in your build graph (typically you re-export it from a styles barrel). - Variables defined inside [`defineConfig`](/docs/api/config/) are picked up automatically; the file imports apply only to standalone `defineVariables` calls. Turn on `strict: true` (or `'warn'`) in `defineConfig` to surface these as build errors instead of silently passing them through. ### Build error: "Unknown frontmatter key" (contributors only) This applies only when editing the salty-css.dev documentation site itself, not when using Salty CSS in your own project. The website docs parser validates frontmatter against a fixed allowlist — if you added a custom key to a doc page, either remove it or extend [`docs-content.ts`](https://github.com/margarita-form/salty-css-website/blob/main/src/lib/docs-content.ts) to accept it. ### `npx salty-css init` picked the wrong framework Re-run with the framework set explicitly via the package manager — `init` looks at the dependencies it can see. If you're in a monorepo, run it from the package's own root, not the workspace root. If it already wrote the wrong config, deleting `salty.config.ts` and re-running is the cleanest fix. ### React Server Components / SSR - Salty CSS works in **server components** — styles are emitted at build time, so there's no runtime dependency to ship to the server. The `styled` function returns a regular React component you can render anywhere. - Where you do need `"use client"`: any component that uses runtime React features (state, effects, event handlers) — including a theme toggle — must be a client component, but the styled component it uses can stay in server land. - Hydration mismatches around theming are usually a flash-of-wrong-theme issue: the server renders one theme attribute, the client toggle script applies another. See the [Theming](/docs/theming/) page for the inline pre-hydration script pattern. ### Finding a generated class in DevTools Every styled component emits a `data-component-name` attribute in development. Open the element inspector, find the attribute (e.g. `data-component-name="Button"`), and the class name on that element is the Salty-generated hash. Searching the Styles panel for that hash takes you straight to the rule. ### Importing heavy modules in a `.css.ts` file Salty CSS evaluates `.css.ts` files at build time, so importing big runtime libraries (or anything that touches `window`) can slow down compilation or fail outright. Keep `.css.ts` files focused on style definitions; if you need a value from a heavy module, derive it once and re-export it from a lightweight intermediate file. For modules you genuinely need at build time but want excluded from the Salty bundle, add them to `externalModules` in [`defineConfig`](/docs/api/config/#externalmodules). ### Still stuck? - [Discord](https://discord.gg/R6kr4KxMhP) — community + maintainers. - [GitHub issues](https://github.com/margarita-form/salty-css/issues) — for confirmed bugs and feature requests. --- # Frequently Asked Questions — Astro Source: https://salty-css.dev/docs/astro/faq/ ### About Salty CSS #### What is Salty CSS? Salty CSS is a TypeScript-first, build-time CSS-in-TS library for React, Next.js and Astro. You author styles in `.css.ts` files using a typed `styled()` API, and the compiler turns them into real CSS at build time. The runtime ships with no styling engine — your components just render with the generated class names. #### Is Salty CSS zero-runtime? The styling layer is. Styles, variants, templates, and tokens are resolved by the build, so the only thing that ships at runtime is a small helper that maps your variant props to class names. There is no runtime style serializer, no in-DOM `` + an element with `className={className}` without further wiring. | | `getDynamicStylesCss` | alias of `css` | Deprecated. Kept exported for backward compatibility with the standalone helper that predates `defineRuntime`. Prefer `css`. | #### The `config` argument `config` is `Partial` — the same shape `defineConfig` accepts. The fields the runtime actually uses are `variables`, `templates`, `mediaQueries`, and `modifiers`. The practical recipe is to import your project's existing config and pass it in, so CMS-supplied `{colors.brand}` tokens resolve to the same CSS variables your `styled` components already use: ```ts import { defineRuntime } from "@salty-css/astro/runtime"; import config from "../../salty.config"; const runtime = defineRuntime(config); ``` `defineRuntime()` (with no argument) is fine when the incoming styles don't reference any tokens or templates. ### Feature support `defineRuntime` runs the same parser as the build-time compiler, so most Salty features come along for free. The exception is anything that requires a component wrapper — `defineRuntime` returns CSS, not a component. | Feature | Supported | Notes | | --------------------------------------------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Token substitution (`{colors.brand}`) | yes | Pass the matching `variables` in `config`. | | Media queries (`@media (...)`) | yes | Scoped under the resolved class — `.${className} { ... }` inside the `@media` block. | | Modifiers / pseudo-classes (`&:hover`, `&[data-state]`) | yes | Ampersand-expansion is identical to the build-time parser. | | Nested selectors (`'& > svg'`) | yes | Combinators are appended to the scope class. | | Templates (`textStyle: 'caption'`) | yes | Pass the matching `templates` in `config`. | | `variants` / `compoundVariants` / `anyOfVariants` | partial | The parser emits the variant selectors (`.${className}.size-sm`, etc.), but there is **no prop → class mapping** here. Either pass a flat object, or select the active branch yourself before calling `resolve`. | | `defaultVariants`, `defaultProps`, `passProps`, `element`, `as` | no | These are `styled()` component concepts. `defineRuntime` returns strings. | | `keyframes`, `defineGlobalStyles`, `defineFont` | no | Build-time only — they need to live in the generated stylesheet, not a per-request `

{block.heading}

{block.body}

); } ``` The class is a hash of `block.css`, so two blocks with identical styles produce the same class — if you want to ship one rule per unique shape, collect the pairs across the request and dedupe by `className` before rendering the ` {children} ); } ``` `{colors.text.onBrand}` resolves against your project config; the `&:hover` and `@media` blocks compose exactly the way they do inside a `styled()` definition. ### Scoping `resolve(styles)` defaults the scope to `.${className}`, which is what you want most of the time — render the returned CSS in a ` next to an element with className={className} ``` Because the same parser runs, JSON-shaped input gets the full Salty surface for free: `{colors.brand}` tokens resolve to the same CSS variables your `styled` components use; `@media`, `&:hover`, and nested selectors all keep working; and two blocks with structurally-identical payloads collapse to a single rule via the shared hash. A CMS block effectively ships "CSS in JSON" and slots straight into your design system — no second pipeline, no escape-hatch global CSS file, and no client styling runtime appearing on the page just because some rules were resolved per-request. The CSS is emitted as a string into the HTML response, and the browser parses it the way it parses your static stylesheet. Reach for this when content authors want power — give them the parser, not a constrained subset. See the [`defineRuntime` API reference](/docs/api/runtime/) for the full surface: signature, feature-support table, scoping, and worked examples. #### Smart Hashing and Deduplication The compiler uses deterministic hashing (`toHash`). If a `HeroBlock` and a `TextBlock` independently define identical style objects, they generate the exact same hash. The CSS rules are emitted to the final stylesheet only once, automatically deduplicating your output and keeping the CSS payload minimal. ### Execution over Extraction (esbuild vs. AST) To extract styles at build time, a compiler has to understand your code. There are two broad families for doing this, and they make different tradeoffs. The first is **Abstract Syntax Tree (AST) static analysis**, used to great effect by tools like Panda CSS (and, historically, by Babel-based extractors like Linaria). These are genuinely impressive pieces of engineering — modern static extractors do identifier resolution, constant folding, and partial evaluation, and they're fast and battle-tested in large codebases. The tradeoff is inherent to the approach: the analyzer reasons about your code without running it. When a value can only be known at runtime — a prop driven by state, a token resolved through an indirection the analyzer can't follow, a style object built by a sufficiently dynamic helper — the extractor has to either skip it or pre-generate every plausible combination ahead of time. That's why static tools offer escape hatches (Panda's `staticCss`, for instance) to declare the dynamic surface up front. It's a perfectly reasonable model; it just asks you to keep your styles statically analyzable, and to reach for those hatches when you can't. Salty CSS sits in the other family: **it executes rather than analyzes.** Instead of parsing an AST, the Salty compiler uses `esbuild` to actually evaluate your `.css.ts` files in a Node environment during the build step. This is the same fundamental idea Vanilla Extract pioneered with its `.css.ts` model, and Salty owes it a clear debt. Because the code is genuinely executed: - You can safely import variables, functions, or design tokens from anywhere in your codebase. - You can write complex helper functions to generate style objects. - You can even use `async/await` inside your style definitions (e.g., fetching a remote design token registry during the build). If your code runs in Node, Salty CSS can compile it. The cost of this approach is the constraint it places on _where_ styles live — which is the subject of the next section — but in exchange you don't have to keep your styling logic statically legible to a parser. ### The Compilation Pipeline Knowing _why_ Salty CSS executes code rather than parsing ASTs is useful context. Knowing _what actually happens_ between saving a file and seeing a styled component in the browser is what you need when something breaks — or when you're trying to understand why a particular file is slowing your build down. There are two distinct journeys to trace. The first is the **build-time funnel**: what runs when you start the dev server or kick off a production build. The second is the **load-time funnel**: what the bundler does with a `.css.ts` file when it encounters it as an import, and what the browser ends up with. #### Funnel 1 — Build time: `.css.ts` → `saltygen/index.css` ``` Plugin hook → Discovery → Config pass → esbuild (per file) → import() → CSS generation → layer assembly → index.css ``` **1. Plugin hook fires** The bundler plugin (`saltyPlugin` for Vite, the webpack loader, `withSaltyCss` for Next.js) hooks into the bundler's `buildStart`. It kicks off a full generation, which begins by wiping `saltygen/` clean and recreating its working subdirectories from scratch. Everything flows from this point on a fresh slate. **2. File discovery** A recursive walk of the project tree skips `node_modules` and `saltygen/` entirely. A file counts as salty if its name matches `*.(salty|css|styles|styled).(ts|tsx|js|…)`. Within that set, any file that also calls a `defineX(` factory is additionally flagged as a _config file_ and queued for processing first. Two sets leave this step: all salty files, and the config-file subset. > **Perf:** This is a synchronous depth-first filesystem walk across the whole project. In a large monorepo with thousands of directories, the syscall overhead is measurable. The compiler can't skip arbitrary output directories — only `node_modules` and `saltygen/` are excluded by name. Keep generated directories out of your source tree, and avoid nesting `.css.ts` files inside paths that share a prefix with other deeply populated directories. > Note: performance optimizations are on the roadmap **3. Config pass (parallel)** Config files run before anything else — they declare the design tokens and templates that every component file depends on. Each one goes through the same per-file compile step as everything else (see step 4), and its exports are sorted by what they declare: variables, templates, media queries, fonts, imports, and globals. `salty.config.ts` itself is compiled here as well, and its output is merged in. The output of this pass is written into `saltygen/`: the global CSS layers (variables, reset, global, templates, fonts, imports), the generated TypeScript token/template/media-query types, and a serialised snapshot of the merged config — tokens, templates, media queries — that later steps can read without re-running the compiler. **4. Per-file esbuild compilation** This is the step that makes Salty CSS an _execution-based_ compiler, and where build performance is most sensitive. Before esbuild ever sees the source, two things happen to it. First, any `styled(SomeNonSaltyComponent, …)` call is rewritten to `styled('div', …)`, so non-salty components don't drag their import chains into the bundle. Second, the cached config is made available to the file at evaluation time, so each file can resolve design tokens without re-compiling the config. The patched source is then fed to **esbuild via stdin** (not as a file on disk), bundled, tree-shaken, and transpiled to a Node-compatible format. The result is written into `saltygen/js/` under a deterministic, content-derived filename — so an unchanged file produces the same path and can be reused by the incremental HMR path. > **Perf:** esbuild is written in Go and is extremely fast — but it follows every `import` chain in the file. `packages: 'external'` excludes `node_modules`, but _local_ barrel files are a different matter. If your design tokens live in `./tokens/index.ts` and that file re-exports 50 individual token modules, esbuild follows all 50 chains for every `.css.ts` that imports it. Each file gets its own esbuild invocation, so a barrel with 50 re-exports across 80 style files means 4 000 extra module parses. Import from the specific token file you need, not a catch-all barrel. **5. Dynamic `import()` + export bucketing** The compiled `.js` is loaded into the running Node process with a dynamic `import()`. This is the step that separates Salty CSS from static AST tools: the code is _executed_ — any logic you wrote runs, any imports resolve, any `async/await` settles. The exports come back as a live JavaScript object. Each named export is then inspected to work out what it is — a keyframes definition, a className generator, or a styled generator — and bucketed accordingly. Exports that resolve asynchronously (a bare `Promise`, or a function flagged to be awaited) are settled before they're bucketed. > **Perf:** You _can_ `await fetch(...)` inside a style definition to pull tokens from a remote registry at build time. Each unresolved `await` serialises evaluation within that file's module. If 30 components each independently fetch from the same endpoint, you wait for 30 sequential requests. Batch-fetch once in a shared import and let the other files import the result instead — esbuild will inline the value, and the fetch runs once. **6. CSS generation** For each generator, the build-time context is locked in: the export name becomes the CSS filename prefix (`Button` → `cl_button-TzHVd-0.css`) and, in development, the value of the `data-component-name` attribute. The generator then resolves and serialises its styles — which means it: - Resolves `{token.path}` references to `var(--token-path)` CSS custom properties - Expands template call sites (`textStyle: 'heading.large'` → the class that the template layer emits) - Handles `@media` blocks, pseudo-selectors, and modifier rewrites declared in your config - Serialises the result to a CSS string The **hash** is computed from the style content itself — base, variants, and compound variants — not from the export name. It's content-addressed: rename the export and nothing changes; edit a style value and the hash changes. Two components with structurally identical style objects produce the same hash and collapse to a single CSS rule, deduplicated automatically. > **Note on cascade order:** A `styled()` call whose first argument is another styled component increments `priority` by one. Higher-priority generators land in a later CSS layer (`l1`, `l2`, …). Because the `@layer` order is declared at the top of `index.css`, the cascade is deterministic: a more-specific component's rules always win over its base's rules, regardless of where the files live on disk or what order they were compiled in. **7. Layer assembly + `index.css`** Component CSS files are grouped by priority and merged into `l_0.css`, `l_1.css`, and so on — each wrapped in `@layer l0 { … }`. Each per-component block is delimited by `/*start:-*/` and `/*end:*/` markers. These markers are how the HMR incremental path patches a single component's CSS without triggering a full rebuild. `index.css` is then assembled as the single entry point: a `@layer imports, reset, global, templates, fonts, l0, l1, …;` declaration at the top, followed by `@import` rules for each global file and each layer file. This is the file the user's app imports. --- #### Funnel 2 — Load time: `saltygen/` → rendered component The build funnel produces the CSS. This second funnel traces what happens when the bundler processes a `.css.ts` _import_ in your component tree, and what the browser ultimately receives. ``` Bundler load hook → source transform → minified client file → browser renders ``` **1. Bundler load hook** When the bundler encounters a `.css.ts` import, the plugin's `load` hook fires. For React projects it runs a framework-specific transform that is loaded lazily and memoised for the entire bundler session, so the transform module loads once regardless of how many salty files exist. **2. Source transform** The per-file compile step runs again on the file (reusing the cached `.js` from `saltygen/js/` if the content hasn't changed). For each styled or className export, the generator's pre-computed client metadata is serialised — `{ hash, variantKeys, propValueKeys, defaultProps, attr }` — and the source is rewritten in place: the full style object is replaced with the baked-in client form. ```ts // Your source export const Button = styled("button", { base: { padding: "{spacing.medium}" }, variants: { intent: { primary: { background: "{theme.primary}" } } }, }); ``` ```ts // What the bundler serves to the browser export const Button = styled("button", "TzHVd", { hash: "TzHVd", variantKeys: ["intent"], propValueKeys: [], defaultProps: {}, attr: { "data-component-name": "Button" }, // stripped in production }); ``` Import paths are also rewritten: `{ styled } from "@salty-css/react/styled"` becomes `{ styledClient as styled } from "@salty-css/react/styled-client"`. All style logic — the `base` object, token references, variant definitions — is gone from the bundle. **3. Browser receives** The client bundle contains no CSS serialisation engine, no CSSOM injection, no theme provider. The HTML element carries hashed class names (`class="TzHVd intent-primary"`). The static `saltygen/index.css` — already linked in `` — provides `.TzHVd { … }` and `.TzHVd.intent-primary { … }`. The browser applies them via its native cascade. Nothing computes anything. **4. React render — server-first path** In a React Server Component or an Astro component, there is no client bundle for that component at all. `styledClient` runs on the server, emits a string of class names baked into the HTML, and the component contributes zero JavaScript to the client payload. This is the "genuinely zero-runtime" case from the opening of this page. **5. React render — client component path** `styledClient` is the thin mapper that ships to the browser when you mark a component `'use client'`. It receives the pre-compiled client metadata, reads variant props at render time (e.g. `intent="primary"`), and appends the matching class name alongside the base hash class. If any `css-*` props are present (like `css-bg-color="tomato"`), it writes the corresponding CSS custom property as an inline style. No new classes are generated, no ` {children} ); } ``` What ships to the browser is a `)}` in an RSC, or `{[...pairs.values()].map((css) => ``` What ships to the browser is a `)}` in an RSC, or `{[...pairs.values()].map((css) =>