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

→ Every salty-css command, argument and flag — and what each one writes.

CLI

The CLI ships inside @salty-css/core, so if Salty is in your package.json it's already installed. Nothing to add globally, nothing separate to keep in version sync.

Example
npx salty-css [command] [options]

Four commands. init sets a project up, generate scaffolds a file with the right suffix, build compiles the CSS on demand, and update moves every @salty-css/* package to the same version. None of them is required to use Salty — every file they write, you could write by hand.

The package installs the binary under four names — salty-css, salty, @salty-css/core, and salty-css-cjs for environments that need the CommonJS entry. They all run the same thing; this page uses salty-css. Node 22 or newer is required.

Commands

CommandAliasArgumentsWhat it does
init[directory]Install packages, write the config, wire the plugin, run the first build.
generateg[file] [directory]Scaffold a component file with the right suffix and boilerplate.
buildb[directory]Compile the project's CSS.
updateup[version]Move every Salty package to one version.

-h, --help works on the program and on each command — npx salty-css build --help prints the same flags documented below. -v, --version is a program-level option rather than a command; it's covered near the end of this page.

Where you run it decides what it acts on. Every path resolves against the current working directory, and .saltyrc.json — the file that tells the CLI where your projects are — is read from there too, rather than searched for up the tree. So a bare npx salty-css build works from the directory holding that file, and from anywhere else you pass the project directory yourself.

Two conveniences worth knowing before the details: if node_modules/.bin/prettier exists, every file the CLI writes or edits is formatted with your Prettier config afterwards. And nothing overwrites an existing file — commands skip what's already there rather than replacing it, which is what makes init safe to re-run.

init

Example
npx salty-css init [directory]

Detects what kind of project you're in, installs the packages that match, writes the config files, edits your bundler config, and runs a first build so saltygen/ exists before you write a component. It's the one command most projects run once, and the one that touches the most files.

Argument / optionWhat it does
[directory]Project directory, relative to where you run the command. Defaults to ..
-d, --dir <dir>The same thing as the argument. Wins if you pass both.
--css-file <css-file>The existing CSS file to add the @import to, relative to the project directory. Skips the search described below.
--skip-installDon't install anything. Everything else still runs.
-y, --yesSkip the install confirmation prompt.

It needs a package.json in the directory you run it from, and it errors out rather than guessing if there isn't one.

What it writes

The full diff, in the order it happens:

  • salty.config.ts in the project directory, with strict already on.
  • saltygen/index.css as a placeholder, so the import you're about to add resolves before the first build finishes.
  • .saltyrc.json at the directory you ran the command from, with an entry for this project — the file that tells the CLI where your projects are, and one of the two you commit.
  • .gitignore gains a saltygen entry, if you have a .gitignore and it doesn't already mention it.
  • A stylesheet import@import '…/saltygen/index.css'; prepended to a global CSS file.
  • Your bundler config, edited in place by whichever integrations matched.
  • package.json gains "prepare": "npx salty-css build". If you already have a prepare script, && npx salty-css build is appended to it instead; if it already mentions salty-css, nothing changes.
  • The first build, so the CSS and the generated types exist immediately.

It finishes by printing the next steps. Configuration is where both generated config files are documented.

Re-running is safe. Existing files are left alone, the .gitignore and stylesheet edits are idempotent, an already-wired bundler config is skipped, and .saltyrc.json only gains an entry for a directory it doesn't already list — which is what lets you run init once per app in a monorepo and end up with one file describing all of them. It's also why "delete salty.config.ts and re-run" is the fix when detection goes wrong: a clean re-init is much shorter than untangling half-written config by hand.

Frameworks and integrations

Two separate detections happen, and knowing they're separate explains most surprises.

The framework decides which runtime package you get and which templates generate will use later. It's read from your bundler config and your package.json — an astro.config.* file or astro in your dependencies makes it an Astro project, and React is the fallback, which is why a project that's neither still gets a working install.

Integrations decide which config files get edited. They're detected independently and more than one can match:

IntegrationDetected byWhat it does
ESLinteslint.config.js, eslint.config.mjs, or .eslintrc.json, in the project directory or the directory you ran fromAdds @salty-css/eslint-config-core to your config.
Vitevite.config.tsInserts saltyPlugin(__dirname) into plugins.
Next.jsnext.config.js, .cjs, .ts, or .mjsWraps your export in withSaltyCss(...).
Webpackwebpack.config.jsApplies saltyPlugin(config, __dirname) to your exported config.
Astroastro.config.mjs, .ts, .js, or .cjsAdds the Salty integration to integrations.

Next.js matches the Next integration rather than the Webpack one, since withSaltyCss already wraps Webpack and Turbopack for you.

Each integration edits by pattern-matching your existing config, which works on config files shaped the way the ecosystem usually shapes them and gives up loudly otherwise: when it can't work out where to insert, it warns, leaves the file untouched, and tells you to add it yourself. Worth reading the terminal output rather than assuming silence means success.

What gets installed

@salty-css/core and the framework runtime (@salty-css/react or @salty-css/astro) as dependencies, plus one dev dependency per matched integration — @salty-css/next, @salty-css/vite, @salty-css/webpack, @salty-css/astro, @salty-css/eslint-config-core.

All of them are pinned to the version of the CLI you ran. That's deliberate: the packages are built and released as a set, and this is the cheapest way to make sure they start out in lockstep. (update is how they stay that way.)

The install shells out to npm, and it prompts first — it prints the list and waits for y. Two consequences:

  • On pnpm, yarn, or bun, run init --skip-install and add the printed packages with your own package manager. Everything else init does still happens.
  • Without an interactive terminal the prompt can't run, so the command fails with a message telling you to re-run with --yes. In CI, --yes or --skip-install.

The stylesheet import

With the default importStrategy: 'root', one stylesheet has to be imported once at your app root, and init goes looking for a file to put it in. It checks src, public, assets, styles, css, and app, then a styles, css, app, or pages folder inside each of those, for a file named index, styles, main, app, global, or globals with a .css, .scss, or .sass extension. First hit wins.

That covers the usual layouts — src/index.css, app/globals.css, src/styles/main.scss — and misses anything unusual. When it misses, it warns and leaves the import to you. --css-file skips the search entirely and is the better move if you already know the file:

Example
npx salty-css init --css-file src/app/globals.css

generate

Example
npx salty-css generate [file] [directory]

Scaffolds a styled component in a file the compiler will actually read. The whole point is the filename: .css.ts versus .ts is the difference between styles and no styles, and it's the mistake worth designing out rather than remembering.

Argument / optionWhat it does
[file]Path of the file to create, relative to the project directory. Required.
[directory]Project directory. Falls back to defaultProject in .saltyrc.json.
-f, --file <file>The same as the first argument.
-d, --dir <dir>The same as the second.
-t, --tag <tag>The HTML tag to render. Defaults to div.
-n, --name <name>The export name. Defaults to the filename in PascalCase.
-c, --className <className>Adds a className to the generated call.
-r, --reactComponentAlso scaffold a wrapper component file. See below.

The path is fixed up for you: a missing extension becomes .ts, and .css is inserted before it unless the name already ends in .css. So components/button lands as components/button.css.ts, and components/button.css.tsx is left alone. Missing directories are created. An existing file is never overwritten — you get an error and nothing is written.

How you define it

Example
npx salty-css generate components/button --name Button --tag button
components/button.css.ts
import { styled } from "@salty-css/react/styled";

export const Button = styled("button", {
  base: {
    // Add your styles here
  },
});

That's the whole file — an exported, correctly-named component with an empty base waiting for you. --className adds a className key alongside it, and the framework recorded in .saltyrc.json decides whether you get the React or the Astro import.

How you use it

Example
import { Button } from "./components/button.css";

export const SaveBar = () => <Button type="submit">Save</Button>;

Note the import path: ./components/button.css, without the .ts. The full picture of which suffix to use when, and where these files tend to live as a project grows, is in File structure.

The -r pair

-r splits the work across two files instead of one — the styled definition, and a React component that renders it:

Example
npx salty-css generate components/card -t section -r
components/card.css.ts
import { styled } from "@salty-css/react/styled";

export const CardWrapper = styled("section", {
  base: {
    // Add your styles here
  },
});
components/card.tsx
import { CardWrapper } from "./card.css";

interface CardComponentProps {
  text?: string;
}

export const CardComponent = ({ text = "Lorem ipsum" }: CardComponentProps) => {
  return <CardWrapper>{text}</CardWrapper>;
};

export default CardComponent;

The naming is the part to notice: the styled export becomes <Name>Wrapper and the component <Name>Component, because the two now live in the same folder and can't share a name. On Astro the wrapper is a .astro file instead.

Reach for this when a component needs real logic around its markup — props, state, a bit of composition — and keep the single-file form when it doesn't. Unlike the other commands, generate doesn't need a package.json, so it also works in a bare package inside a monorepo.

build

Example
npx salty-css build [directory]

Compiles every Salty file it can reach and regenerates saltygen/ from scratch.

Argument / optionWhat it does
[directory]Project directory. Falls back to defaultProject in .saltyrc.json.
-d, --dir <dir>The same as the argument.
--watchStay open and rebuild on change.
--mode <mode>production or development. Defaults to detection from NODE_ENV. Any other value is an error.

With a bundler plugin wired up, your dev server already does this on save and you'll rarely type it. It earns its place in four places: the prepare script, a CI step, debugging a build you want to run in isolation, and any setup where the plugin isn't running at all.

Example
npx salty-css build --watch                # rebuild on change, no dev server
npx salty-css build --mode production      # what actually ships

--mode is worth knowing about because development and production builds differ in one visible way: dev builds render data-component-name on every styled element, production builds strip it. If you're about to write a selector against production HTML, this is the build to check it against — Testing goes further into that.

--watch watches the project directory recursively and splits changes two ways. A change to a component file recompiles that one file; a change to salty.config.ts, or to any Salty file containing a defineX( or keyframes( call, triggers a full rebuild — those define things everything else references, so they can't be recompiled in isolation. The test is a plain text match on the file's contents, which means a defineVariables( sitting in a comment costs you a full rebuild for nothing.

It's the same split the bundler plugins use, with the same caveat: the incremental path appends and patches, it never prunes. Stale rules after a rename or a delete mean "rebuild", not "the compiler is wrong."

saltygen/ is a build artifact whichever way it gets built — don't commit it, don't hand-edit it, and don't write a test against its internals. File structure covers what ends up in there.

update

Example
npx salty-css update [version]
npx salty-css up [version]

Bumps every @salty-css/* package in your package.json together, then offers to rebuild.

Argument / optionWhat it does
[version]The version to move to. Defaults to latest.
-v, --version <version>The same as the argument.
-y, --yesSkip both prompts — the install and the rebuild.
-d, --dir <dir>Directory to rebuild afterwards. Falls back to defaultProject.
--legacy-peer-depsPassed straight through to npm. Not recommended — it quiets a version conflict rather than resolving one.
Example
npx salty-css up             # everything to latest
npx salty-css up 0.4.2       # or pin them all to one version

Together is the important word. @salty-css/core, @salty-css/react and your bundler plugin are built and released as a set, and mixing versions across them produces confusing failures rather than clear ones — a component that renders with no styles, a token path that type-checks and compiles to nothing. Bumping one by hand is the shortest route to an afternoon you hadn't planned on.

What it actually does: reads your dependencies and devDependencies for anything named salty-css or @salty-css/*, installs them all at the target version with npm, prints what each one ended up at, then asks whether to rebuild — because saltygen/ was generated by the old compiler and wants regenerating by the new one. Starting your dev server does that too.

Two ways it stops early, both harmless: no Salty packages in the package.json it can see means you're in the wrong directory, and it says so rather than installing anything. No --dir and no defaultProject means it skips the rebuild with a warning and leaves you to run build yourself.

Salty is pre-1.0, so minor APIs can still move between releases. Pin your versions rather than floating on a caret, read the release notes before you bump, and you'll be fine. Updates is the guide version of this command.

--version

Example
npx salty-css --version

Prints the version of the CLI that's running, then every Salty package in your package.json with the range you have declared:

Example
info:    CLI is running: 0.4.2
info:    @salty-css/core: ^0.4.2
info:    @salty-css/react: ^0.4.2

-v is the short form, and running npx salty-css with no command at all prints the same thing. This is the output to paste into a bug report — mismatched versions explain enough failures that it's the first thing worth ruling out.

Running it in CI

Three things behave differently without an interactive terminal, and all three have a flag:

  • init fails rather than installing without asking. --yes accepts the package list up front; --skip-install skips the install entirely.
  • update skips the rebuild silently and installs anyway. --yes takes both prompts.
  • prepare doesn't always run. Some platforms install with --ignore-scripts, which skips it — add npx salty-css build to the build step explicitly there. It's also the honest way to run the compiler in isolation when you're debugging one.