Package Exports
- @antebudimir/eslint-plugin-vanilla-extract
Readme
@antebudimir/eslint-plugin-vanilla-extract
An ESLint plugin for enforcing best practices in vanilla-extract CSS styles, including CSS property ordering and additional linting rules. Available presets are for alphabetical and concentric CSS ordering. The plugin also supports a custom group ordering option based on groups available in concentric CSS.
Demo
Features
- Enforces CSS property ordering in vanilla-extract style objects with two available presets:
- Alphabetical ordering for clean, predictable style organization
- Concentric ordering for logical, outside-in property arrangement
- Custom group ordering option for more fine-grained control
- Built for ESLint 9 flat config system
- Provides auto-fix capability to automatically sort properties
- Handles multiple vanilla-extract APIs (style, styleVariants, recipe, globalStyle, etc.)
- Handles complex cases like nested objects, arrays of styles, and pseudo selectors
- Works with camelCase properties as used in vanilla-extract
- Additional linting rules for enhanced code quality (see roadmap for upcoming features)
Requirements
- ESLint 9.0.0 or higher
- Node.js 18.18.0 or higher
- ESM (ECMAScript Modules) only
Installation
# Using npm
npm install --save-dev @antebudimir/eslint-plugin-vanilla-extract
# Using yarn
yarn add --dev @antebudimir/eslint-plugin-vanilla-extract
# Using pnpm
pnpm add -D @antebudimir/eslint-plugin-vanilla-extractUsage
Note: This plugin is ESM-only. It must be used with ESM configurations and can't be used with CommonJS require().
ESLint Flat Config (ESLint 9+)
Create or update your eslint.config.js or eslint.config.mjs file:
import vanillaExtract from '@antebudimir/eslint-plugin-vanilla-extract';
// Using the recommended configuration
export default [
{
files: ['**/*.css.ts'],
ignores: ['src/**/theme-contract.css.ts'],
plugins: {
'vanilla-extract': vanillaExtract,
},
rules: {
// Apply all recommended rules
...vanillaExtract.configs.recommended.rules,
// Optionally override specific rules
// 'vanilla-extract/concentric-order': 'warn', // Change severity from error to warn
// 'vanilla-extract/no-empty-style-blocks': 'off', // Disable a recommended rule
// 'vanilla-extract/no-zero-unit': 'warn', // Change severity from error to warn
// Add additional rules not in recommended config
// 'vanilla-extract/alphabetical-order': 'error', // Override concentric-order rule
},
},
];Recommended Configuration
The recommended configuration enables the following rules with error severity:
vanilla-extract/concentric-order: Enforces concentric CSS property orderingvanilla-extract/no-empty-style-blocks: Prevents empty style blocksvanilla-extract/no-unknown-unit: prohibits usage of unrecognized CSS units.vanilla-extract/no-zero-unit: removes unnecessary units for zero values
You can use the recommended configuration as a starting point and override rules as needed for your project.
Custom Configuration
If you prefer not to use the recommended configuration, you can still configure rules manually:
export default [
{
files: ['**/*.css.ts'],
ignores: ['src/**/theme-contract.css.ts'],
plugins: {
'vanilla-extract': vanillaExtract,
},
rules: {
// 'vanilla-extract/alphabetical-order': 'warn',
// OR
// 'vanilla-extract/concentric-order': 'warn',
// OR
'vanilla-extract/custom-order': [
'warn',
{
groupOrder: ['font', 'dimensions', 'margin', 'padding', 'position', 'border'], // example group order
// optional
sortRemainingProperties: 'concentric', // 'alphabetical' is default
},
],
'vanilla-extract/no-unknown-unit': 'error',
'vanilla-extract/no-zero-unit': 'warn',
},
},
];Rules
vanilla-extract/alphabetical-order
This rule enforces that CSS properties in vanilla-extract style objects follow alphabetical ordering.
// ❌ Incorrect
import { style } from '@vanilla-extract/css';
export const myStyle = style({
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: 19,
marginBottom: 1,
marginLeft: 2,
});
// ✅ Correct
import { style } from '@vanilla-extract/css';
export const myStyle = style({
alignItems: 'center',
display: 'flex',
height: 19,
justifyContent: 'center',
marginBottom: 1,
marginLeft: 2,
});vanilla-extract/concentric-order
This rule enforces that CSS properties in vanilla-extract style objects follow the concentric CSS ordering pattern, which organizes properties from outside to inside.
// ❌ Incorrect
import { style } from '@vanilla-extract/css';
export const myStyle = style({
color: 'red',
display: 'flex',
position: 'relative',
padding: '10px',
margin: '20px',
});
// ✅ Correct
import { style } from '@vanilla-extract/css';
export const myStyle = style({
position: 'relative',
display: 'flex',
margin: '20px',
padding: '10px',
color: 'red',
});vanilla-extract/custom-order
The vanilla-extract/custom-order rule enables you to enforce a custom ordering of CSS properties in your vanilla-extract styles. You can specify an array of property groups in your preferred order, and the rule will ensure that properties within these groups are sorted according to their position in the concentric CSS model.
Key features of this rule include:
- Custom group ordering: Define your preferred order of CSS property groups.
- Handling of unspecified groups: All groups not included in the custom array will have their properties sorted after the last specified group.
- Flexible sorting options: You can choose to sort remaining properties either alphabetically or following the concentric CSS order by setting the
sortRemainingPropertiesoption to 'alphabetical' or 'concentric' respectively.
Default behavior:
- If not set,
sortRemainingPropertiesdefaults to 'alphabetical'. - If no
groupOrderis specified or an empty array is provided, the rule will default to sorting all properties alphabetically, andsortRemainingPropertieswill be ignored even if set.
To configure the rule, add it to your ESLint configuration file with your desired options. You can customize the groups array to include any number of available CSS property groups you want to enforce, with a minimum of one group required.
// ❌ Incorrect (Unordered)
import { style } from '@vanilla-extract/css';
export const myStyle = style({
color: 'blue',
padding: '10px',
fontFamily: 'Arial, sans-serif',
margin: '20px',
width: '200px',
border: '1px solid black',
display: 'flex',
});
// ✅ Correct
import { style } from '@vanilla-extract/css';
export const myStyle = style({
// font group
fontFamily: 'Arial, sans-serif',
color: 'blue',
// dimensions group
width: '200px',
// margin group
margin: '20px',
// padding group
padding: '10px',
// display group
display: 'flex',
// border group
border: '1px solid black',
});vanilla-extract/no-empty-style-blocks
This rule detects and prevents empty style blocks in vanilla-extract stylesheets. It helps maintain cleaner codebases by eliminating empty style definitions that often result from incomplete refactoring or forgotten implementations.
// ❌ Incorrect
import { style } from '@vanilla-extract/css';
export const emptyStyle = style({});
export const nestedEmpty = style({
color: 'blue',
':hover': {},
'@media': {
'(min-width: 768px)': {},
},
});
export const recipeWithEmptyVariants = recipe({
base: { color: 'black' },
variants: {},
});
// ✅ Correct
import { style } from '@vanilla-extract/css';
export const nestedEmpty = style({
color: 'blue',
});
export const recipeWithEmptyVariants = recipe({
base: { color: 'black' },
});vanilla-extract/no-unknown-unit
This rule enforces the use of valid CSS units in vanilla-extract style objects. It prevents typos and non-standard units that could cause styling issues or browser compatibility problems.
// ❌ Incorrect
import { style, globalStyle, recipe } from '@vanilla-extract/css';
export const invalidStyle = style({
margin: '5abc', // Non-existent unit
fontSize: '1.5rems', // Typo in unit
});
export const myRecipe = recipe({
variants: {
size: {
large: { padding: '4xm' } // Invalid unit
}
}
});
// ✅ Correct
import { style, globalStyle, recipe } from '@vanilla-extract/css';
export const validStyle = style({
margin: '5rem',
fontSize: '1.5rem',
});
export const myRecipe = recipe({
variants: {
size: {
large: { padding: '4em' }
}
}
});vanilla-extract/no-zero-unit
This rule enforces the removal of unnecessary units for zero values in vanilla-extract style objects. It helps maintain cleaner and more consistent CSS by eliminating redundant units when the value is zero.
// ❌ Incorrect
import { style } from '@vanilla-extract/css';
export const myStyle = style({
margin: '0px',
padding: '0rem',
width: '0%',
height: '0vh',
top: '-0em',
});
// ✅ Correct
import { style } from '@vanilla-extract/css';
export const myStyle = style({
margin: '0',
padding: '0',
width: '0',
height: '0',
top: '0',
});Font Face Declarations
For fontFace and globalFontFace API calls, all three ordering rules (alphabetical, concentric, and custom) enforce the same special ordering:
- The
srcproperty always appears first - All remaining properties are sorted alphabetically
This special handling is applied because:
- The
srcproperty is the most critical property in font face declarations - Consistent ordering improves readability for these specific APIs
- Font-related properties are specialized and benefit from standardized ordering
// ✅ Correct ordering for font faces
export const theFont = fontFace({
src: ['url("/fonts/MyFont.woff2") format("woff2")', 'url("/fonts/MyFont.woff") format("woff")'],
ascentOverride: '90%',
descentOverride: '10%',
fontDisplay: 'swap',
fontFeatureSettings: '"liga" 1',
fontStretch: 'normal',
// ...other properties in alphabetical order
});Opinionated, but it is what it is. If someone has a suggestion for a better ordering, let me know!
Concentric CSS Model
Here's a list of all available groups from the provided concentricGroups array:
- boxSizing
- position
- display
- flex
- grid
- alignment
- columns
- transform
- transitions
- visibility
- shape
- margin
- outline
- border
- boxShadow
- background
- cursor
- padding
- dimensions
- overflow
- listStyle
- tables
- animation
- text
- textSpacing
- font
- content
- counters
- breaks
These groups represent different categories of CSS properties, organized in a concentric order from outside to inside. Each group contains related CSS properties that affect specific aspects of an element's styling and layout.
Roadmap
The roadmap outlines the project's current status and future plans:
Completed Milestones
- Initial release with support for alphabetical, concentric, and custom group CSS ordering.
- Auto-fix capability integrated into ESLint.
- Support for multiple vanilla-extract APIs (e.g.,
style,styleVariants,recipe,globalStyle,fontFace, etc.). no-empty-style-blocksrule to disallow empty blocks.- Recommended ESLint configuration for the plugin.
no-zero-unitrule to disallow units when the value is zero.no-unknown-unitrule to disallow unknown units.- Comprehensive rule testing.
Current Work
- Support for using the plugin’s recommended config via the extends field (as discussed in issue #3)
Upcoming Features
no-number-trailing-zerorule to disallow trailing zeros in numbers.no-px-unitrule to disallow use ofpxunits with configurable whitelist.prefer-logical-propertiesrule to enforce use of logical properties.prefer-theme-tokensrule to enforce use of theme tokens instead of hard-coded values when available.no-global-stylerule to disallow use ofglobalStylefunction.property-unit-matchrule to enforce valid units per CSS property specs. Note: This feature will only be implemented if there's sufficient interest from the community.- Option to sort properties within user-defined concentric groups alphabetically instead of following the concentric order. Note: This feature will only be implemented if there's sufficient interest from the community.
Contributing
All well-intentioned contributions are welcome, of course! Please feel free to submit a Pull Request or get in touch via GitHub issues.
License
This project is licensed under the MIT License - see the LICENSE file for details.