Version 0.3.0 is out with internal changes. Read more from GitHub Releases

For Astro

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.

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
---
// src/pages/index.astro
import { Button } from "../components/button.css";
---

<Button>Pass the salt</Button>
In a React island
// src/islands/Cta.tsx
import { Button } from "../components/button.css";

export function Cta() {
  return <Button>Pass the salt</Button>;
}

Same component. Two runtimes. One stylesheet.

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. Styles live in dedicated .css.ts files by convention — that's the boundary the compiler keys off. 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 <html> and the variables cascade natively into every island below it — React, Vue, static — because that's just how the cascade works. No Context bridge to thread through islands, and no re-render when the theme flips.

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
// 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
/* 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 <html>and the theme’s CSS variables cascade into every island underneath it. There’s no ThemeProviderto 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. Astro Server Islands slot into this cleanly too, if you reach for them — a nice bonus, not the headline.

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. No second pipeline, no client styling runtime.

---
// 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",
});
---

<article class={className}>
  <style set:html={css}></style>
  <slot />
</article>

data-theme on <html> cascades to all islands — React, Vue, static.

From the theming concept doc

Where to next?