Some features documented here might be available only with `dev` version tag in NPM

ESLint

Salty CSS ships a small ESLint plugin and a matching shareable config. Two rules; both are autofixable; both only run on Salty files (.css.ts, .css.tsx, .salty.ts, .styles.ts, .styled.ts).

Why

The build-time compiler has two blind spots a linter can fix:

  1. It only picks up exports. A styled(...) (or className, keyframes, any defineX(...)) call that isn't exported is invisible dead code — you'll wonder why your component has no styles. The plugin catches it.
  2. variants must be a sibling of base, not nested inside it. Nesting compiles to something — just not what you wanted. The plugin catches that too.

Install

npm i -D @salty-css/eslint-plugin-core @salty-css/eslint-config-core

Set it up

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

export default [
  saltyConfig,
  // ...your other configs
];

The flat config imports the plugin internally — you don't need to register @salty-css/core yourself.

Legacy config (ESLint 8 and older)

// .eslintrc.js
module.exports = {
  extends: ["@salty-css/eslint-config-core"],
};

The legacy config declares plugins: ['@salty-css/core']. ESLint resolves that to @salty-css/eslint-plugin-core, which is why you install both packages.

Both shapes enable both rules at 'error' severity for .ts and .tsx files. The rules themselves check the filename before running, so non-Salty TypeScript files aren't affected.

Rules

@salty-css/core/must-be-exported

  • Default severity: error
  • Autofixable: yes (eslint --fix inserts export or export default)
  • Scope: .css.ts, .css.tsx, .salty.ts, .styles.ts, .styled.ts

Flags any styled, className, keyframes, or defineX* call that isn't exported.

✗ Bad

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

const Button = styled("button", { base: { padding: "1rem" } }); // ← unexported, compiler ignores it

✓ Good

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

export const Button = styled("button", { base: { padding: "1rem" } });

The same applies to top-level defineGlobalStyles, defineVariables, defineTemplates, defineFont, defineImport, defineMediaQuery, etc. If you don't want a name, the autofix inserts export default.

@salty-css/core/no-variants-in-base

  • Default severity: error
  • Autofixable: yes (the fix lifts variants out of base)
  • Scope: same as above

Flags variants declared inside base. The compiler expects variants as a sibling.

✗ Bad

export const Card = styled("div", {
  base: {
    padding: "1rem",
    variants: {
      // ← wrong: nested inside base
      tone: { brand: { background: "{colors.brand.main}" } },
    },
  },
});

✓ Good

export const Card = styled("div", {
  base: {
    padding: "1rem",
  },
  variants: {
    tone: { brand: { background: "{colors.brand.main}" } },
  },
});

Overriding severity

Drop a rule to a warning, or turn it off entirely, the same way you'd override any ESLint rule:

// eslint.config.mjs (flat)
import saltyConfig from "@salty-css/eslint-config-core/flat";

export default [
  saltyConfig,
  {
    rules: {
      "@salty-css/core/no-variants-in-base": "warn",
      "@salty-css/core/must-be-exported": "off",
    },
  },
];

Troubleshooting

If the rules don't fire on a file you expect them to, check the filename suffix — the plugin only inspects files matching the Salty extensions. Renaming my-component.ts to my-component.css.ts makes both the compiler and the linter pick it up. See Troubleshooting for the broader file-extension story.