@daviderenfeld/eslint-plugin-strict-tailwind-order
v1.10.0
Published
Strict Tailwind CSS class ordering with custom CSS detection, family-safe wrapping, and multi-page Tailwind v4 themes
Downloads
358
Maintainers
Readme
@daviderenfeld/eslint-plugin-strict-tailwind-order
ESLint rules for strict Tailwind CSS class ordering in Vue, JSX, TSX, and class helper calls.
The fixer preserves every class token exactly. It can reorder tokens and wrap long static class attributes, but it does not add, remove, rename, merge, normalize, or replace classes.
Install with pnpm
pnpm add -D @daviderenfeld/eslint-plugin-strict-tailwind-orderExisting Vue ESLint configuration
Use this preset when the project already configures Vue and TypeScript parsers:
import strictTailwindOrder from '@daviderenfeld/eslint-plugin-strict-tailwind-order';
export default defineConfigWithVueTs(
existingConfigs,
skipFormatting,
strictTailwindOrder.configs['flat/vue-existing-config'],
);This preset does not define or replace parsers, language options, globals, ignores, or existing ESLint rules.
Enabled rules
{
'strict-tailwind-order/strict-tailwind-order': 'error',
'strict-tailwind-order/report-unknown-classes': 'warn',
'strict-tailwind-order/stylesheet-errors': 'warn',
}Recommended multi-page configuration
For repositories whose files are stored under pages/01, pages/02, and so on, use the {page} placeholder:
{
files: ['pages/**/*.{vue,js,ts,jsx,tsx}'],
settings: {
strictTailwindOrder: {
stylesheet: 'pages/{page}/src/main.css',
maxClassLineLength: 100,
},
},
}A file under pages/02/src/... will read pages/02/src/main.css. Custom colors, font sizes, and breakpoints are therefore resolved from the correct page instead of always reading page 01.
For explicit selection, use ordered glob mappings. The first match wins:
settings: {
strictTailwindOrder: {
stylesheets: {
'pages/01/**': 'pages/01/src/main.css',
'pages/02/**': 'pages/02/src/main.css',
},
},
},A single-page project can continue using a normal path:
settings: {
strictTailwindOrder: {
stylesheet: 'pages/01/src/main.css',
},
},Custom CSS class detection
When a stylesheet is configured, project-defined classes are detected automatically from ordinary CSS selectors and Tailwind v4 @utility declarations.
.project-card {
position: relative;
}
.ntlv-scrollbar-none::-webkit-scrollbar {
display: none;
}
@utility content-auto {
content-visibility: auto;
}
@utility tab-* {
tab-size: --value(integer);
}The final order is:
custom CSS classes → Tailwind utilities → unknown classesFor example:
class="mt-4 typo-class project-card bg-white flex ntlv-scrollbar-none"becomes:
class="project-card ntlv-scrollbar-none flex bg-white mt-4 typo-class"Detected custom classes are not reported by report-unknown-classes. Their original relative order is preserved at the beginning.
Relative local imports are followed recursively by default:
@import './components/cards.css';
@import './utilities/scrollbar.css';
@import 'tailwindcss';Local files are scanned. Package imports and remote URLs are ignored. Import cycles are handled safely, and scanning stops at maxImportDepth, which defaults to 10.
@utility definitions support known Tailwind variants:
class="hover:content-auto tab-4"Ordinary selector classes are matched exactly. A class such as hover:project-card remains unknown unless that exact escaped selector exists in CSS.
Disable detection or import traversal when needed:
settings: {
strictTailwindOrder: {
stylesheet: 'pages/{page}/src/main.css',
detectCustomClasses: false,
followImports: false,
maxImportDepth: 0,
},
},Unknown variants and breakpoints
The whole class is treated as unknown when its variant is not recognized:
sxl:flexIt is moved to the absolute end and reported by report-unknown-classes.
The same class remains valid when sxl is declared in the selected Tailwind stylesheet:
@theme {
--breakpoint-sxl: 110rem;
}It can also be supplied through breakpointOrder or the rule-level breakpoints option.
Official state and arbitrary variants remain recognized, including hover:*, motion-reduce:*, group-*, peer-*, aria-*, data-*, supports-*, ui-*, and [&...]:*.
Family-safe line wrapping
Static Vue and JSX class attributes are wrapped when their class content exceeds 100 characters by default:
<div
class="
w-full flex flex-col items-center lg:items-start text-center lg:text-start
motion-reduce:opacity-100 px-4
"
/>Wrapping rules:
- The limit counts only the class content, not
class=""or indentation. - A line of exactly 100 characters is not wrapped.
- Breaks occur only between utility families.
- Base and responsive members of one family stay together, such as
items-center lg:items-start. - A single family may exceed the limit rather than being split.
- Set
maxClassLineLength: 0to disable wrapping.
Ordinary quoted JavaScript strings are sorted but are not line-wrapped, because inserting a raw newline would make them invalid JavaScript. Static template literals may be wrapped safely.
Prettier order
Core Prettier collapses whitespace inside a static class attribute. To preserve this plugin's family-safe wrapping, run ESLint after Prettier in formatting or pre-commit workflows.
Example order:
{
"*.vue": [
"prettier --write",
"eslint --fix -c config/eslint.config.ts"
]
}Do not enable prettier-plugin-tailwindcss at the same time, because it applies a different Tailwind ordering algorithm.
Available settings
stylesheet: one Tailwind v4 stylesheet path; supports{page}.stylesheets: file-glob-to-stylesheet mapping; first match wins.stylesheetErrors:'report'or'ignore'.breakpointOrder: explicit responsive breakpoint order.maxClassLineLength: default100; use0to disable wrapping.detectCustomClasses: detects CSS selector and@utilityclasses; defaulttrue.followImports: follows relative local CSS@importchains; defaulttrue.maxImportDepth: maximum local import depth; default10.
Stylesheet behavior
- Custom Tailwind v4 font sizes are extracted from
--text-*theme variables. - Custom colors are extracted from
--color-*variables. - Custom breakpoints are extracted from
--breakpoint-*variables. - Text metadata variables such as
--text-hero--line-heightdo not create separate size tokens. --font-*variables are not treated as text sizes.- Mixed breakpoint units are not converted automatically.
- Explicit
breakpointOrderoverrides inferred ordering. - Theme declarations and custom classes are read from the selected stylesheet and followed relative local imports.
- Package imports and remote URLs are not followed.
- Import cycles are deduplicated safely.
Stylesheet errors
Use the dedicated rule for severity:
rules: {
'strict-tailwind-order/stylesheet-errors': 'warn',
}Suppress stylesheet problems entirely with:
settings: {
strictTailwindOrder: {
stylesheetErrors: 'ignore',
},
}Ignore non-CSS project hooks
{
rules: {
'strict-tailwind-order/report-unknown-classes': [
'warn',
{
ignore: ['hide-scrollbar', 'no-shadow', 'video-btn-clean'],
},
],
},
}Use ignore only for tokens that are intentionally used as JavaScript, analytics, or external-library hooks but are not defined in the project CSS. Ignored classes are omitted from warnings and remain at the end.
Supported syntax
- Vue static
classattributes - Vue Transition class attributes
- Vue static strings inside dynamic
:class - Vue arrays, objects, conditionals, and logical expressions in
:class - Static strings inside
cn,clsx,classNames,twJoin, andtwMerge - JSX
className - Responsive, state, stacked, and arbitrary variants
- Official
will-change-*utilities - Arbitrary CSS properties such as
[will-change:transform]and[-webkit-text-stroke:...]
Dynamic template literals and concatenated strings are ignored.
Detected custom CSS classes move to the beginning, Tailwind utilities follow, and unknown classes or unknown variants move to the end. Relative order is preserved inside the custom and unknown groups.
Run
Check without changing files:
pnpm exec eslint . -c config/eslint.config.ts --no-cacheApply fixes:
pnpm exec eslint . --fix -c config/eslint.config.tsTest
pnpm install
pnpm testPublish a new version
pnpm test
pnpm pack --dry-run
pnpm publish --access public