# Salty CSS for Astro > Typed styled components and token autocomplete for Astro and its React islands — Salty's compiler extracts everything to plain CSS at build time. This file contains the Salty CSS documentation for the Astro framework, extracted for LLM ingestion. Includes framework-specific docs, shared concepts, and Astro-compatible recipes. Source: https://salty-css.dev/llms-full-astro.txt — Generated: 2026-06-23 Full file (all frameworks): https://salty-css.dev/llms-full.txt Companion index: https://salty-css.dev/llms.txt --- # Salty CSS for Astro Source: https://salty-css.dev/astro/ > Typed styled components and token autocomplete for Astro and its React islands — Salty's compiler extracts everything to plain CSS at build time. The part of Astro you chose it for. The fun part of CSS-in-JS. Typed `styled` components and token autocomplete — extracted to plain CSS, so the browser gets nothing but the stylesheet. Astro is half the reason Salty exists — native zero-runtime styling that belonged here was rare, so it got built. ### Get started ```bash npx salty-css init ``` Then read the [Installation guide](/docs/astro/installation/) or the [Quick Start](/docs/astro/quick-start/). ### What you get **Build-time extraction** — Astro hands your `.css.ts` files to Salty's compiler during the build, which runs them and writes the result into one plain, cached stylesheet. Nothing but CSS reaches the browser; no styling engine ships. **React islands, included** — A styled component imported in a React island and the same component used in an `.astro` page resolve to the exact same compiled stylesheet. There's no second pipeline and no duplicated rules — identical style objects even dedupe to a single class. One file, one stylesheet, two runtimes. **Theming across frameworks** — Themes are plain CSS custom properties, not JavaScript state. Set `data-theme` on `` and the variables cascade natively into every island below it — React, Vue, static — because that's just how the cascade works. ### Same styled API, two homes Define the component once. Use it from your Astro template or from a React island — the stylesheet is shared, the markup isn't. In `.astro`: ```astro --- // src/pages/index.astro import { Button } from "../components/button.css"; --- ``` In a React island: ```tsx // src/islands/Cta.tsx import { Button } from "../components/button.css"; export function Cta() { return ; } ``` Same component. Two runtimes. One stylesheet. ### What compiles to what No AST guesswork. The compiler runs your `.css.ts` file, hashes the styles it produces, and emits a plain rule into the static stylesheet — that hash is the class name your markup wears. You write: ```ts // src/components/button.css.ts import { styled } from "@salty-css/astro/styled"; export const Button = styled("button", { base: { padding: "0.75rem 1.25rem", borderRadius: "6px", background: "{theme.color}", }, }); ``` The browser gets: ```css /* saltygen/index.css (generated) */ .c_button-AbC12 { padding: 0.75rem 1.25rem; border-radius: 6px; background: var(--theme-color); } ``` A tiny hash and static CSS. No styling engine rides along. ### Why styles live in `.css.ts` files Keeping styles in dedicated `.css.ts` files is a real tradeoff — you give up colocating them inside the component, and for some teams that stings. What it buys back: a compilation firewall. The compiler only evaluates style files, never your whole component tree, so builds stay fast and their cost tracks how much styling you have, not how big your app gets. And because a `.css.ts` file holds zero framework runtime logic, the same primitives port across Astro, Next, and Vite without dragging JSX or render cycles along. ### Why Salty for Astro? Astro is broad — static pages, React islands, server rendering. Salty is built to sit comfortably across all of it, not just one corner. **Islands-first theming, no provider** — Set `data-theme` on `` and the theme's CSS variables cascade into every island underneath it. There's no `ThemeProvider` to mount, no Context to bridge across island boundaries, and no re-render when someone flips light to dark — it's the browser's cascade doing the work, not a JavaScript state machine. **Genuinely zero-runtime on the server** — For a server-rendered `.astro` component, nothing but hashed class names and the static stylesheet reaches the browser — no style injection, no variant mapper, no theme runtime. The component contributes zero JavaScript to the client payload. **defineRuntime for CMS-driven content** — When the style object itself isn't known until a request — a tenant tint, a CMS-authored block — call the same parser the compiler uses, right from Astro frontmatter. Tokens, media queries, and nested selectors all keep working, and the CSS is emitted as a string into the response. ```astro --- // src/components/TenantCard.astro — runs per request import { runtime } from "../lib/runtime"; const { tenant } = Astro.props; const { className, css } = await runtime.resolve({ background: tenant.brandPrimary, // from your CMS / DB color: "{colors.text.onBrand}", padding: "1.5rem", }); ---
``` `data-theme` on `` cascades to all islands — React, Vue, static. ### Is it the right fit? Salty is built for design-led, dynamic Astro sites — the kind where you're crafting bespoke components and feeding them from a CMS. If what you actually want is the smallest possible stylesheet for a lean, repetitive dashboard, atomic CSS may genuinely be the better fit. ### Where to go next - [Installation](/docs/astro/installation/) — Add the Salty integration to `astro.config` and start the dev server. - [Quick Start](/docs/astro/quick-start/) — From `npx salty-css init` to a styled component rendering in an `.astro` page. - [Component styles](/docs/astro/basics/) — Build typed primitives with the styled API and learn how they compose. - [Variants](/docs/astro/variants/) — Drive a component's looks from typed props with variants and compound variants. - [Templates](/docs/astro/templates/) — Share reusable style recipes across components with defineTemplates. --- # Documentation — Astro Source: https://salty-css.dev/docs/astro/ 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/astro/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 — Astro Source: https://salty-css.dev/docs/astro/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 — Astro Source: https://salty-css.dev/docs/astro/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/astro/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 : ```astro --- // src/pages/index.astro import { Component } from "../components/my-component.css"; --- This is a Salty CSS component ``` 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/astro/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: ```astro --- // src/pages/index.astro import { Button } from "../components/button/button.css"; ---
``` 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: ```astro --- // /src/components/ThemeToggle.astro --- ``` 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 ` ``` What ships to the browser is a `)}` in an RSC, or `{[...pairs.values()].map((css) =>