The part of Astro you chose it for. The fun part of CSS-in-JS.
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.
.astro--- // src/pages/index.astro import { Button } from "../components/button.css"; --- <Button>Pass the salt</Button>
// 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
React islands, included
Theming across frameworks
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.
// 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}", }, });
/* 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>
From the theming concept doc
data-themeon<html>cascades to all islands — React, Vue, static.