Skip to content

margarita-form/salty-css

Repository files navigation

Salty CSS Banner

Salty CSS — CSS-in-JS that compiles away

Salty CSS is a build-time, type-safe CSS-in-JS library. You author styles in .css.ts files; the compiler emits real CSS, your runtime ships zero styling logic.

Get started · API index · Docs · Discord · GitHub · NPM

Features

  • Build-time compilation — no runtime style injection, no FOUC, no client bundle cost.
  • Framework support — Next.js (App + Pages, React Server Components, Webpack & Turbopack), React + Vite, React + Webpack, Astro.
  • Type safety — TypeScript-first authoring, generated token types, an ESLint plugin.
  • Design tokens & theming — static, responsive (media-bound), and conditional (data-attribute / class-bound) variables in one place.
  • Templates — reusable style bundles with their own variants.
  • Modifiers — custom value transformers ('space:3''12px') defined in config.
  • Variants, compound variants, anyOf variants, default variants out of the box.

Get started

npx salty-css init

init detects your framework, installs the right packages, creates salty.config.ts, and wires the bundler plugin. Per-framework setup:

React + Webpack (without Next.js) is also supported via @salty-css/webpack.

CLI

Command Alias What it does
npx salty-css init [directory] Detect framework, install packages, create salty.config.ts, wire the plugin.
npx salty-css generate [filePath] g Scaffold a new Salty component file. Flags: --name, --className, --tag.
npx salty-css build [directory] b Compile *.css.ts files into saltygen/index.css. Not needed when the bundler plugin is running. Flags: --watch, --mode.
npx salty-css update [version] up Update all @salty-css/* packages. Defaults to latest. Flags: --dir, --yes.
npx salty-css --version Print CLI version.

Full reference: salty-css.dev/docs/cli.

Good to know

  1. File extensions matter. styled, className, keyframes, and every defineX(...) call must live in a file ending .css.ts, .css.tsx, .salty.ts, .styles.ts, or .styled.ts. The compiler ignores everything else.
  2. Extending non-Salty components is finestyled(NextLink, { ... }) — as long as the wrapped component forwards className to a DOM element.
  3. Async values & functions are allowed (base: { color: async () => 'red' }), but heavy imports inside *.css.ts slow the build.
  4. React Server Components are supported via @salty-css/next — no 'use client' needed for styled output.

ESLint

Salty CSS ships a small ESLint plugin and matching shareable config. Two rules, both error by default, both autofixable, both scoped to Salty files (.css.ts, .css.tsx, .salty.ts, .styles.ts, .styled.ts):

  • @salty-css/core/must-be-exported — every styled, className, keyframes, and defineX* call must be exported; the compiler ignores anything else.
  • @salty-css/core/no-variants-in-basevariants must be a sibling of base, not nested inside it.
npm i -D @salty-css/eslint-plugin-core @salty-css/eslint-config-core

Flat config (ESLint 9+):

// eslint.config.mjs
import saltyConfig from '@salty-css/eslint-config-core/flat';

export default [saltyConfig];

Legacy (.eslintrc):

module.exports = { extends: ['@salty-css/eslint-config-core'] };

Full reference → salty-css.dev/docs/eslint.

API index

Symbol Import One-liner Docs
styled @salty-css/react/styled React component factory with variants & extension. api/styled
className @salty-css/react/class-name CSS class string with .variant() chaining. api/classname
Variants variants, compoundVariants, anyOfVariants, defaultVariants semantics. variants
Overrides Extend components, swap element, override per-instance via style. overrides
ESLint plugin @salty-css/eslint-config-core Two rules: enforce export and correct variants placement on Salty files. eslint
defineConfig @salty-css/core/config Top-level project config. api/config
defineVariables @salty-css/core/factories Static, responsive, and conditional CSS variables (tokens). variables
Theming data-theme switcher built on conditional variables. theming
defineGlobalStyles @salty-css/core/factories Global rules (html, body, etc.). api/define-factories
defineMediaQuery @salty-css/core/factories Named, reusable media queries. media-queries
defineTemplates @salty-css/core/factories Reusable style bundles, optionally with variants. templates
defineFont @salty-css/core/factories @font-face (or @import) + CSS variable in one. fonts
defineImport @salty-css/core/factories Pull external CSS into Salty's imports layer. imports
keyframes @salty-css/react/keyframes Typed @keyframes with params and initial-state injection. animations
defineViewportClamp @salty-css/core/helpers Fluid clamp() values that scale with the viewport. viewport-clamp
color @salty-css/core/helpers Color manipulation (alpha, darken, …). color-function
Modifiers (on defineConfig) Custom value transformers, e.g. 'space:3''12px'. modifiers
withSaltyCss @salty-css/next Next.js config wrapper (Webpack + Turbopack). installation
saltyPlugin (Vite) @salty-css/vite Vite plugin. installation
saltyPlugin (Webpack) @salty-css/webpack Webpack loader + plugin. installation
saltyIntegration @salty-css/astro/integration Astro integration. installation

styled

// components/button.css.ts
import { styled } from '@salty-css/react/styled';

export const Button = styled('button', {
  className: 'btn', // optional custom class
  element: 'button', // optional element override
  base: {
    padding: '0.6em 1.2em',
    border: '1px solid currentColor',
    '&:hover': { background: 'black', color: 'white' },
  },
  variants: {
    variant: {
      outlined: {},
      solid: { background: 'black', color: 'white' },
    },
  },
  compoundVariants: [
    /* { variant: 'solid', size: 'lg', css: { ... } } */
  ],
  defaultVariants: { variant: 'outlined' },
  defaultProps: { type: 'button' },
  passProps: false, // pass variant props through to the rendered element
  priority: undefined, // 0–8, higher wins; usually leave to auto
});
<Button variant="solid">Save</Button>

See full reference → salty-css.dev/docs/api/styled.

className

Framework-agnostic class-string factory. The return value is a string you can drop in className=, plus a .variant(name, value) method for chaining variant classes.

// components/card.css.ts
import { className } from '@salty-css/react/class-name';

export const card = className({
  className: 'card',
  base: {
    padding: '1rem',
    borderRadius: '8px',
  },
  variants: {
    tone: {
      neutral: { background: '#f6f6f6' },
      brand: { background: '{colors.brand.main}' },
    },
  },
});
<div className={`${card.variant('tone', 'brand')}`}>Hello</div>

See full reference → salty-css.dev/docs/api/classname.

Variants

styled and className share the same variant shape:

  • variants — named axes (e.g. size: { sm, md, lg }). Each axis becomes a prop.
  • compoundVariants — array; styles applied only when all listed axes match.
  • anyOfVariants — array; styles applied when any listed axis matches.
  • defaultVariants — values used when the prop is omitted at the call site.
styled('a', {
  base: { textDecoration: 'none' },
  variants: {
    size: { sm: { fontSize: 14 }, lg: { fontSize: 18 } },
    underline: { true: { textDecoration: 'underline' } },
  },
  compoundVariants: [{ size: 'lg', underline: true, css: { textUnderlineOffset: 4 } }],
  defaultVariants: { size: 'sm' },
});

Boolean variants accept true/false keys; pass them as <Link underline /> shorthand. See salty-css.dev/docs/variants.

Overrides

  • Extend any component: styled(ExistingComponent, { ... }). The wrapped component must accept className.
  • Swap the rendered element for one instance: <Button as="a" href="/x" /> (use passProps if you want variant props to reach the underlying element).
  • Per-instance style overrides: <Box style={{ ... }} /> and <Box css={{ ... }} /> accept regular React style and Salty's css prop (overrides apply at the highest priority).

See salty-css.dev/docs/overrides for as, style, CSS-variable overrides, and ref forwarding.

defineConfig

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

export const config = defineConfig({
  importStrategy: 'root', // 'root' | 'component'
  variables: {
    /* see defineVariables */
  },
  global: {
    /* see defineGlobalStyles */
  },
  templates: {
    /* see defineTemplates */
  },
  modifiers: {
    /* see Modifiers */
  },
  reset: 'default', // 'default' | 'none' | GlobalStyles
  externalModules: ['react', 'react-dom'],
  strict: true, // true | 'warn' | false
  defaultUnit: 'px', // px, rem, em, vh, vw, % …
});

Full reference → salty-css.dev/docs/api/config.

defineVariables

Tokens come in three flavours — static, responsive (media-bound), conditional (class/data-attribute-bound):

// styles/variables.css.ts
import { defineVariables } from '@salty-css/core/factories';

export default defineVariables({
  colors: {
    dark: '#111',
    light: '#fefefe',
    brand: { main: '#0070f3', highlight: '#ff4081' },
  },
  responsive: {
    base: { fontSize: { heading: '48px', body: '16px' } },
    '@largeMobileDown': { fontSize: { heading: '32px', body: '14px' } },
  },
  conditional: {
    theme: {
      dark: { backgroundColor: '{colors.dark}', textColor: '{colors.light}' },
      light: { backgroundColor: '{colors.light}', textColor: '{colors.dark}' },
    },
  },
});

Use as string references: {colors.brand.main}, {fontSize.heading}, {theme.textColor}.

See salty-css.dev/docs/variables.

Theming

The conditional bucket above wires up data-attribute / class-based themes with zero providers. Toggle a data-theme attribute on <html> (or any ancestor) and conditional variables resolve to the matching branch:

<html data-theme="dark">
  <!-- '{theme.textColor}' resolves to '{colors.light}' -->
</html>

See the dark-mode walkthrough (with SSR flash fix) → salty-css.dev/docs/theming.

defineGlobalStyles

// styles/global.css.ts
import { defineGlobalStyles } from '@salty-css/core/factories';

export default defineGlobalStyles({
  html: { fontFamily: 'Inter, system-ui, sans-serif' },
  body: { margin: 0, background: '#fff' },
});

defineMediaQuery

// styles/media.css.ts
import { defineMediaQuery } from '@salty-css/core/factories';

export const largeMobileDown = defineMediaQuery((media) => media.maxWidth(600));
export const darkMode = defineMediaQuery((media) => media.prefersColorScheme('dark'));
styled('span', { base: { fontSize: 64, '@largeMobileDown': { fontSize: 32 } } });

See salty-css.dev/docs/media-queries.

defineTemplates

Reusable style bundles. Static templates pick values by dot-path; function templates take a parameter.

// styles/templates.css.ts
import { defineTemplates } from '@salty-css/core/factories';

export default defineTemplates({
  textStyle: {
    heading: {
      base: { fontFamily: '{fontFamily.heading}', lineHeight: 1.1 },
      variants: {
        weight: { regular: { fontWeight: 500 }, heavy: { fontWeight: 800 } },
      },
      large: { fontSize: '{fontSize.heading.large}' },
      small: { fontSize: '{fontSize.heading.small}' },
    },
  },
  card: (padding: string) => ({
    padding,
    borderRadius: 8,
    boxShadow: '0 0 10px rgba(0,0,0,0.1)',
  }),
});
styled('h1', { base: { textStyle: 'heading.large@weight=heavy', card: '20px' } });

Rich-node variants, inheritance, and the compoundVariants/anyOfVariants rules: salty-css.dev/docs/templates.

defineFont

// styles/fonts.css.ts
import { defineFont } from '@salty-css/core/factories';

export const Inter = defineFont({
  name: 'Inter',
  variable: '--font-inter',
  display: 'swap',
  fallback: 'system-ui, sans-serif',
  variants: [
    { weight: 400, style: 'normal', src: '/fonts/inter-400.woff2' },
    { weight: 700, style: 'normal', src: ['/fonts/inter-700.woff2', '/fonts/inter-700.ttf'] },
  ],
});

// Or pull a remote stylesheet (e.g. Google Fonts) — emits @import + variable.
export const InterCdn = defineFont({
  name: 'Inter',
  variable: '--font-inter',
  import: 'https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap',
});

Inter.className, Inter.variable, Inter.fontFamily, and Inter.style are available for explicit usage. See salty-css.dev/docs/fonts.

defineImport

Pull external CSS into Salty's imports cascade layer (which sits before reset, global, templates, and your component layers — so your styles always win).

// styles/imports.css.ts
import { defineImport } from '@salty-css/core/factories';

export default defineImport(
  './reset.css', // relative
  'modern-normalize/modern-normalize.css', // node_modules
  '~normalize.css/normalize.css', // node_modules (~ form)
  '/fonts/inter.css', // public/ folder
  'https://fonts.googleapis.com/css2?family=Inter', // URL
  { url: './print.css', media: 'print' }, // media-conditional
  { url: './p3.css', supports: 'color(display-p3 1 1 1)' }, // supports-conditional
);

Layer order: @layer imports, reset, global, templates, fonts, l0…l8;. See salty-css.dev/docs/imports.

keyframes

// styles/animations.css.ts
import { keyframes } from '@salty-css/react/keyframes';

export const fadeIn = keyframes({
  animationName: 'fadeIn',
  appendInitialStyles: true, // injects `from`/`0%` as base styles on the component
  params: { delay: '250ms', fillMode: 'forwards' },
  from: { opacity: 0 },
  to: { opacity: 1 },
});
styled('div', { base: { animation: fadeIn } });

See salty-css.dev/docs/animations.

defineViewportClamp

// styles/clamp.css.ts
import { defineViewportClamp } from '@salty-css/core/helpers';

export const fhdClamp = defineViewportClamp({ screenSize: 1920 });
export const mobileClamp = defineViewportClamp({ screenSize: 640, axis: 'horizontal' });
styled('span', { base: { fontSize: fhdClamp(96), '@largeMobileDown': { fontSize: mobileClamp(48) } } });

Options: screenSize, axis ('horizontal' | 'vertical'), minMultiplier, maxMultiplier, minMaxUnit. See salty-css.dev/docs/viewport-clamp.

color

import { color } from '@salty-css/core/helpers';
import { styled } from '@salty-css/react/styled';

export const Tint = styled('span', {
  base: { backgroundColor: color('#000').alpha(0.5) },
});

Backed by the Qix-/color library — .alpha(), .darken(), .lighten(), .mix(), etc. See salty-css.dev/docs/color-function.

Modifiers

Custom value transformers registered on defineConfig. Each modifier is a { pattern, transform } pair: when Salty sees a string value matching pattern, it replaces it with transform(match).value (and optionally emits extra CSS via transform(match).css).

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

export const config = defineConfig({
  modifiers: {
    spaceShorthand: {
      pattern: /^space:(\d+)$/,
      transform: (match) => {
        const n = Number(match.replace('space:', ''));
        return { value: `${n * 4}px` };
      },
    },
  },
});
styled('div', { base: { padding: 'space:3' } }); // → padding: 12px

See salty-css.dev/docs/modifiers.

withSaltyCss (Next.js)

// next.config.ts
import { withSaltyCss } from '@salty-css/next';

const nextConfig = {
  /* your config */
};
export default withSaltyCss(nextConfig);
// Or pin a bundler: withSaltyCss(nextConfig, { bundler: 'webpack' })

Options: mode (build mode override), bundler ('auto' | 'webpack' | 'turbopack', defaults to auto-detect via process.env.TURBOPACK), dir (project root for Turbopack — defaults to nextConfig.turbopack.root or process.cwd()).

Next.js 14 CommonJS:

const { withSaltyCss } = require('@salty-css/next');
module.exports = withSaltyCss(nextConfig);

See salty-css.dev/docs/installation.

saltyPlugin (Vite)

// vite.config.ts
import { defineConfig } from 'vite';
import { saltyPlugin } from '@salty-css/vite';

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

Options: { mode }.

saltyPlugin (Webpack)

// webpack.config.js
const { saltyPlugin } = require('@salty-css/webpack');

module.exports = (env, argv) => {
  const config = {
    /* … */
  };
  saltyPlugin(config, __dirname);
  return config;
};

Signature: saltyPlugin(config, dir, isServer?, cjs?, { mode? }).

saltyIntegration (Astro)

// astro.config.mjs
import { defineConfig } from 'astro/config';
import saltyIntegration from '@salty-css/astro/integration';

export default defineConfig({
  integrations: [saltyIntegration()],
});

Options: srcDir (defaults to 'src'), rootDir (defaults to the Astro config root).


Support

Help, questions, or feedback → Join the Salty CSS Discord. Bug reports → GitHub issues.

About

CSS-in-JS library for Next.js, Server Components and Astro

Topics

Resources

License

Stars

13 stars

Watchers

2 watching

Forks

Contributors