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:
- It only picks up
exports. Astyled(...)(orclassName,keyframes, anydefineX(...)) call that isn't exported is invisible dead code — you'll wonder why your component has no styles. The plugin catches it. variantsmust be a sibling ofbase, 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
Flat config (ESLint 9+, recommended)
// 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 --fixinsertsexportorexport 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
variantsout ofbase) - 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.