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

→ Install it, write one file, import it into a page — most of it is one command.

Basic setup

Getting Salty CSS into a project: install it, write one file, import it into a page. On an existing app it's a few minutes of work, and most of that is one command.

Install

Run this from the root of any React, Next.js, Vite, or Astro project:

Example
npx salty-css init

It reads your package.json, picks the framework, installs the right packages, drops a salty.config.ts next to your bundler config, wires the plugin in, and creates an empty saltygen/ — the folder the compiler writes your generated CSS into. It's rebuilt from scratch on every compile, so add it to .gitignore and treat it like any other build output.

If your stack isn't one of those four, the question worth asking isn't whether your framework is supported — it's whether your dev server runs on Vite or Webpack. Compatibility covers where the line actually falls.

That's the install. Skip ahead to your first component — everything below is for when init can't do its job, or when you want to see exactly what it did.

Manual install

The shape is the same in every framework:

  1. Install the plugin package plus @salty-css/core — and @salty-css/react on React-based frameworks.
  2. Wire the plugin into your bundler config.
  3. Add salty.config.ts in the same folder as that bundler config.
  4. Make sure the generated stylesheet is imported once at your app root.
  5. Run the dev server (or npx salty-css build) so saltygen/ exists before the first render.

Which plugin you need, and what it wants:

StackPlugin packageWiring
Next.js — App or Pages Router, Webpack or Turbopack@salty-css/nextwithSaltyCss(nextConfig)
React + Vite@salty-css/vitesaltyPlugin(__dirname)
React + Webpack — custom config, CRA eject, Rspack@salty-css/webpacksaltyPlugin(config, __dirname)
Astro@salty-css/astrothe salty() integration

You'll need Node 18+ and TypeScript 5.x, since .css.ts files are evaluated through TS, plus React 18 or 19 on the React packages. Next.js is supported from 13 (App Router) and tested through 16.2; Vite from 5, Astro from 4.

Next.js

Example
npm i @salty-css/next @salty-css/core @salty-css/react
next.config.ts
import { withSaltyCss } from "@salty-css/next";

const nextConfig = {
  /* your config */
};

export default withSaltyCss(nextConfig);

One wrapper covers both Webpack and Turbopack — there's no separate Turbopack setup to hunt for and no flag to flip. It also imports the generated stylesheet for you, so step 4 is already done. Put salty.config.ts next to next.config.ts.

Server Components need no special handling either: the CSS is a static file by the time SSR runs, so a styled component renders on the server like any other. "use client" is only for components that need real runtime React — a theme toggle with state, say.

React + Vite

Example
npm i @salty-css/vite @salty-css/core @salty-css/react
vite.config.ts
import { defineConfig } from "vite";
import { saltyPlugin } from "@salty-css/vite";

export default defineConfig({
  plugins: [saltyPlugin(__dirname)],
});

Keep salty.config.ts beside vite.config.ts. Build once — run the app, or npx salty-css build — then import the generated stylesheet from your entry CSS:

Example
@import "../saltygen/index.css";

React + Webpack

For hand-rolled setups: a custom config, an ejected CRA, Rspack through its webpack-compatible API. On Next.js use withSaltyCss above — it already wraps Webpack.

Example
npm i @salty-css/webpack @salty-css/core @salty-css/react
webpack.config.js
const { saltyPlugin } = require("@salty-css/webpack");

const config = {
  // your existing webpack config
};

saltyPlugin(config, __dirname); // mutates config in place

module.exports = config;

saltyPlugin doesn't return a new config — it mutates the one you hand it, pushing the loader rule for Salty's file suffixes and registering the hook that generates saltygen/ on build start. Create salty.config.ts beside webpack.config.js and @import the generated stylesheet from the global CSS your entry loads.

Astro

Astro is Vite underneath, so the same machinery applies — but the imports and markup differ enough that it has its own version of these docs. Short version: npm i @salty-css/astro @salty-css/core, then add the salty() integration to astro.config.mjs.

The config file

Whichever path you took, you end up with one of these:

salty.config.ts
import { defineConfig } from "@salty-css/core/config";

export const config = defineConfig({
  // Variables, templates and modifiers move in here as you grow.
});

An empty config is a working config — everything in it has a default, and early on the file just needs to exist where the compiler expects it. Configuration walks through every option for when it stops being empty.

Your first component

How you define it

src/components/button.css.ts
import { styled } from "@salty-css/astro/styled";

export const Button = styled("button", {
  base: {
    padding: "0.6rem 1.2rem",
    borderRadius: "6px",
    border: "none",
    background: "#0d1117",
    color: "#ffffff",
    cursor: "pointer",
    "&:hover": { background: "#1c2128" },
  },
});

Two rules are doing quiet work in there, and breaking either one is most of what goes wrong on day one:

  • The filename suffix. Salty only compiles files ending in .css.ts, .css.tsx, .salty.ts, .styled.ts, or .styles.ts. Name it button.ts and it type-checks perfectly and produces exactly zero CSS.
  • The export. The compiler collects exported calls. An unexported styled(...) emits nothing, and nothing warns you — the ESLint plugin exists largely to catch that one.

styled is the single API published per framework, since a component factory has to hand back a component in your framework's own shape; nearly everything else comes from @salty-css/core/* and behaves identically wherever you use it (framework agnostic APIs has the full split).

Scaffold the file quickly with npx salty-css generate src/components/button --name Button.

How you use it

Example
---
// src/pages/index.astro
import "../../saltygen/index.css";
import { Button } from "../components/button.css";
---

<main>
  <Button>Save changes</Button>
</main>

The generated stylesheet gets imported once, anywhere that loads on every page — here it's the page itself, in a real app it's usually your root layout or global CSS. On Next.js withSaltyCss already handles it, so that first line is one you can drop.

Beyond that there's nothing to wrap, register, or provide. Button behaves like the <button> it renders: onClick, disabled, aria-* and ref all pass through untouched, and the stylesheet was written before the component ever rendered.

Run the dev server and it's on screen. If it isn't, Troubleshooting is ordered by how often each cause is the actual cause, and the filename suffix sits at number one by a wide margin.

Stuck on something troubleshooting doesn't cover? The Discord is the fastest way to get untangled.