A fully open source Tailwind CSS diagnostic CLI for project-wide checks. It is designed to scan a full codebase from the terminal, similar to the react-doctor workflow, and currently supports Tailwind v4 projects.
We were inspired by react-doctor and Tailwind CSS IntelliSense.
Anyone can contribute to it, fork it, or build their own variant on top of it.
- Zero-config default scan with every rule enabled.
- Tailwind v4 diagnostics through Tailwind's own language service.
- Custom Tailwind rules for duplicate utilities, dependency mistakes, accessibility issues, shorthand opportunities, and design-system preferences.
- Component-aware dependency checks for shadcn/ui-style components such as
DialogFooter,Button, andCardFooter. - Conservative unknown-component behavior: unknown components are skipped unless
strictmode is enabled. - Responsive-scope aware checks: base
flexsatisfiessm:flex-row, butsm:flexdoes not satisfy baseflex-col. - Monorepo-friendly input resolution for package paths such as
apps/web. - Built-in ignores for generated output from Next.js, Remix, TanStack Start, Vite, Astro, SvelteKit, Nuxt, Vinxi, Nitro, Storybook, Vercel, Netlify, and common test/build tools.
- Grouped terminal output designed for humans and agents: stable rule ids, file/line locations, deterministic sorting, and non-zero exit code when diagnostics exist.
- Programmatic API via
lintProject().
npx -y twlinter@latest .
npx -y twlinter@latest apps/web
npx -y twlinter@latest "src/**/*.{tsx,html}"The default rule profile runs every rule without requiring a .twlinter.json file. The terminal reporter groups noisy style suggestions so one-command scans stay readable.
Once published, developers should be able to run the package directly with npx:
npx -y twlinter@latest . --verboseThe CLI also supports the explicit lint command:
npx -y twlinter@latest lint .
npx -y twlinter@latest lint "src/**/*.{tsx,html}" --verboseThe npm package and installed CLI command are both twlinter.
git clone <repo-url>
cd twlint
npm installUse the built-in fixture to verify the CLI works:
# List all rules
npm run dev -- --help
# Scan the test fixture
npm run dev -- tests/fixtures/tw-v4-app/src
# Or scan the whole repo (excluding node_modules)
npm run dev -- .
# Run only selected rules while developing a rule
npm run dev -- . --rules canonical-classes,shorthand-classes# Point to your project directory
npm run dev -- /path/to/your/project
# With verbose output and custom rules
npm run dev -- /path/to/your/project --verbose --rules canonical-classes,shorthand-classes
# Override CSS entry point if auto-detection fails
npm run dev -- /path/to/your/project --css-entry /path/to/your/project/src/globals.cssnpm run build
node dist/cli.js tests/fixtures/tw-v4-app/src --verbose| Command | Description |
|---|---|
npm run dev -- ... |
Run the CLI directly via tsx (no build needed) |
npm run build |
Compile TypeScript to dist/ |
npm run test |
Run all tests (Vitest) |
npm run test:watch |
Run tests in watch mode |
npm run typecheck |
TypeScript type checking (tsc --noEmit) |
npm run lint |
Lint source code (oxlint) |
npm run format |
Format source code (oxfmt) |
npm run format:check |
Check formatting without writing |
npm run check |
Run all checks: typecheck + lint + format + test |
When run without a subcommand, twlinter scans the provided paths or glob patterns.
Lint files for canonical Tailwind classes. This is the primary command.
Alias behavior: twlinter can be run with or without the lint subcommand.
| Option | Default | Description |
|---|---|---|
--verbose |
Off | Show per-file details and project info |
-c, --config <path> |
Auto | Path to config file (default: .twlinter.json in project root) |
--rules <rules> |
All rules | Comma-separated rule ids. Overrides config rules. |
--cssEntry <path> |
Auto | Tailwind CSS entry file. Overrides auto-discovery. |
twlinter auto-discovers your CSS entry by searching for files that import tailwindcss. It prefers conventional paths like src/app.css, app/globals.css, and styles/globals.css. If multiple candidates are found, it exits with an error.
Diagnostics are grouped by rule-friendly summary with severity icons:
⚠ The class `h-[350px]` can be written as `h-87.5` (2)
app.tsx:12
app.tsx:15
Found 2 warning(s). Scanned 18 file(s) in 45ms.
With --verbose, shows full file paths and project configuration details.
Long style suggestions, such as class reorder lists, are summarized by default and expanded with --verbose.
The CLI is intentionally friendly to both humans and coding agents:
- One command works for most projects:
twlinter .. - Default rules run every check; the reporter groups and summarizes noisy style suggestions.
- Diagnostics include stable
ruleids in the programmatic API. - Output is grouped by message to make large scans readable.
- Results are sorted by file, line, and column for deterministic diffs.
- Exit code is
1when diagnostics are found and0when clean. --rulesmakes experiments reproducible without editing config files.- Generated framework outputs are ignored by default, including TanStack Start and Vinxi output.
Exit code 1 if any diagnostics are found, 0 otherwise.
twlinter detects monorepo layouts (npm, pnpm, yarn, nx) and handles them correctly. Run from a package root to scope the scan:
twlinter lint apps/webimport { lintProject } from "twlinter";
const result = await lintProject(["."]);
console.log(result.diagnostics);
console.log(
`Scanned ${result.scannedFiles} files in ${result.elapsedMilliseconds}ms`,
);| Field | Type | Description |
|---|---|---|
matchedFiles |
number |
Files matched by glob patterns |
scannedFiles |
number |
Files passed through relevance filter |
elapsedMilliseconds |
number |
Total scan duration |
diagnostics |
Diagnostic[] |
All findings |
project |
ProjectInfo |
Discovered project metadata |
| Field | Type | Description |
|---|---|---|
file |
string |
File path |
line |
number |
Line number |
column |
number |
Column number |
rule |
string |
Rule identifier |
severity |
"warning" | "error" |
Severity level |
message |
string |
Human-readable message |
source |
string |
Diagnostic source |
| Field | Type | Default | Description |
|---|---|---|---|
verbose |
boolean |
false |
Show per-file details |
config |
string |
— | Path to config file |
ignorePatterns |
string[] |
— | Additional glob patterns to skip (merged with defaults) |
classIgnorePatterns |
string[] |
— | Regex patterns for class names to suppress |
maxFileSize |
number |
524288 | Maximum file size in bytes to scan |
rules |
string[] |
All rules | Rules to enable. Defaults to every known rule. |
cssEntry |
string |
Auto | Override the CSS entry point detection |
strict |
boolean |
false |
Emit low-confidence dependency warnings for unknown components |
components |
object |
Built-ins | Component base class map merged over built-in presets |
Default rules run every known check so twlinter . catches everything without configuration. Use --rules or .twlinter.json only when you intentionally want a smaller rule set.
Default rule ids:
class-conflicts
canonical-classes
recommended-variant-order
used-blocklisted-class
shorthand-classes
no-duplicate-utilities
canonical-class-order
prefer-truncate-shorthand
no-important-abuse
no-sr-only-display-conflict
consistent-negative-arbitrary-values
prefer-logical-properties
require-motion-reduce-for-animation
no-orphan-layout-utilities
require-flex-for-flex-utilities
require-grid-for-grid-utilities
warn-ineffective-z-index
require-display-for-sizing
warn-hover-on-disabled
require-focus-visible-for-interactive
warn-incomplete-dark-color-pair
prefer-theme-scale
no-magic-spacing
detect-conflicts-in-template-literals
prefer-design-tokensRun a smaller rule set with --rules or .twlinter.json:
twlinter . --rules canonical-classes,shorthand-classes,prefer-logical-properties| Rule | Default | Description |
|---|---|---|
canonical-classes |
On | Suggests canonical Tailwind class names for arbitrary values |
class-conflicts / no-conflicting-utilities |
On | Detect CSS class conflicts on the same element |
recommended-variant-order |
On | Check variant ordering follows Tailwind conventions |
used-blocklisted-class |
On | Detect usage of blocklisted or legacy classes |
shorthand-classes / prefer-shorthand |
On | Suggest shorthand classes (e.g. w-10 h-10 → size-10) |
no-duplicate-utilities |
On | Detect repeated identical utilities (e.g. p-4 p-4) |
canonical-class-order |
On | Enforce deterministic utility ordering (base → sm → md → lg → xl) |
prefer-truncate-shorthand |
On | Suggest truncate over overflow-hidden text-ellipsis whitespace-nowrap |
no-important-abuse |
On | Warn when !important is overused (>2 per class list) |
no-sr-only-display-conflict |
On | Detect sr-only combined with display utilities |
consistent-negative-arbitrary-values |
On | Enforce -top-[5px] over top-[-5px] syntax |
prefer-logical-properties |
On | Encourage ps-*/pe-* over pl-*/pr-* for RTL support |
require-motion-reduce-for-animation |
On | Require motion-reduce:animate-none for animations |
no-orphan-layout-utilities |
On | Detect items-center/justify-center without flex/grid |
require-flex-for-flex-utilities |
On | Detect flex-col/flex-wrap without flex |
require-grid-for-grid-utilities |
On | Detect grid-cols-*/grid-rows-* without grid |
warn-ineffective-z-index |
On | Detect z-* without relative/absolute/fixed/sticky |
require-display-for-sizing |
On | Detect sizing on inline elements without inline-block |
warn-hover-on-disabled |
On | Detect hover:* on disabled elements |
require-focus-visible-for-interactive |
On | Require focus-visible:* on interactive elements with hover:* |
warn-incomplete-dark-color-pair |
On | Detect light-mode colors without dark:* counterparts |
prefer-theme-scale |
On | Prefer Tailwind design scale over arbitrary values |
no-magic-spacing |
On | Detect spacing values not aligned to the 4px design grid |
detect-conflicts-in-template-literals |
On | Detect duplicate utilities in template literal class names |
prefer-design-tokens |
On | Prefer design tokens over raw hex colors |
Specify rules in .twlinter.json or programmatically via LintOptions.rules. Use comma-separated names with --rules on the CLI when you want to narrow the scan for local debugging.
Aliases: prefer-shorthand → shorthand-classes, no-conflicting-utilities → class-conflicts. Both names work interchangeably.
Configuration is optional. Most projects should start with one command:
twlinter .Create a .twlinter.json file in your project root:
{
"ignorePatterns": ["**/*.stories.*", "**/test/**"],
"classIgnorePatterns": ["w-\\[\\d+px\\]", "h-\\[\\d+px\\]"],
"rules": ["canonical-classes", "class-conflicts"],
"maxFileSize": 100000,
"cssEntry": "src/custom.css",
"strict": false,
"components": {
"DialogFooter": {
"baseClasses": "flex flex-col-reverse gap-2 px-6 sm:flex-row sm:justify-end"
}
}
}| Field | Type | Description |
|---|---|---|
ignorePatterns |
string[] |
Glob patterns to skip (merged with built-in defaults) |
classIgnorePatterns |
string[] |
Regex patterns matched against each diagnostic's class name |
rules |
string[] |
Rules to enable |
maxFileSize |
number |
Maximum file size in bytes |
cssEntry |
string |
Override auto-discovered CSS entry point |
strict |
boolean |
Emit low-confidence dependency warnings for unknown components |
components |
object |
Component base class map used by component-aware rules |
Config can also be placed in package.json under the "twlinter" key.
twlinter ignores generated and dependency output by default, both at glob time and with a hard path-segment filter. This keeps one-command scans useful in real apps and prevents generated framework files from dominating diagnostics.
Ignored path segments include:
node_modules
dist
vendor
.next
.nuxt
.svelte-kit
.astro
.tanstack
.vinxi
.vite
.vercel
.netlify
.output
.nitro
.cache
.parcel-cache
.turbo
build
coverage
out
storybook-static
playwright-reportThis covers common output from Next.js, Remix, React Router, TanStack Start, Vite, Vinxi, Nitro, Astro, SvelteKit, Nuxt, Storybook, Vercel, Netlify, and test tooling.
Some rules need to know whether a class can actually take effect. For example, flex-row only works when the rendered element also has flex or inline-flex.
Native elements are checked directly:
<div className="flex-row" />Known components are checked against their effective classes, which are the component's base classes plus the user-provided classes:
<DialogFooter className="flex-row" />If DialogFooter is configured or included in the built-in shadcn/ui preset with flex, this does not warn.
Unknown components are skipped by default because twlinter cannot know what they render internally. Enable strict to emit low-confidence warnings instead.
{
"strict": true,
"components": {
"Toolbar": {
"baseClasses": "inline-flex items-center gap-2"
},
"Dialog.Footer": {
"baseClasses": "flex flex-col-reverse sm:flex-row sm:justify-end"
}
}
}Component-aware dependency rules also understand responsive scope. A base flex satisfies sm:flex-row, but sm:flex does not satisfy a base flex-col.
ignorePatterns are glob patterns merged into the built-in ignores:
{
"ignorePatterns": ["**/*.stories.*", "**/generated/**", "**/test/**"]
}classIgnorePatterns are regex patterns tested against each diagnostic's class name. Diagnostics whose class matches any pattern are silently dropped:
{
"classIgnorePatterns": [
"w-\\[\\d+px\\]",
"h-\\[\\d+px\\]",
"min-h-\\[\\d+px\\]",
"max-w-\\[\\d+(?:px|rem)\\]"
]
}This suppresses all suggestions for w-[100px], h-[350px], sm:w-[260px], max-w-[12rem], etc. The patterns support variants — sm:w-[260px] matches w-\[\d+px\] because the class name contains the token.
This CLI is not a separate Tailwind parser with a completely different rule engine. It reuses Tailwind's existing language tooling and adapts it for terminal-based project scans.
This package provides the Tailwind analysis engine. It is the part that understands Tailwind syntax, project configuration, and class diagnostics.
This package provides the document model that the language service expects as input. In the editor, Tailwind diagnostics run against open text documents. In this CLI, we create those documents ourselves from files on disk and pass them into the Tailwind language service.
They solve different problems:
@tailwindcss/language-servicedoes the Tailwind-aware analysis.vscode-languageserver-textdocumentrepresents file contents in the language-server format.
So this project is effectively a CLI wrapper around the Tailwind language tooling, not a second unrelated linter.
src/
cli.ts CLI entry (commander)
index.ts Programmatic API
types.ts Shared types
constants.ts Default globs and patterns
formatter.ts Line-level diagnostic formatting
config/
types.ts Config type definitions
load-config.ts Config file loader (.twlinter.json / package.json)
core/
lint-project.ts Scan orchestration
validation-worker.ts Worker thread for parallel validation
normalize-diagnostic.ts
rules.ts Rule registry
shorthand-classes.ts Shorthand detection via canonicalizeCandidates
custom-rules/
context.ts Rule context and component-aware class resolution
index.ts Custom static-analysis rules
utils.ts Shared class extraction, variant/element parsing
discovery/
discover-project.ts
resolve-css-entry.ts
resolve-inputs.ts
detect-monorepo.ts
file-relevance.ts
adapters/
tailwind-language-service.ts
tailwind-design-system.ts
reporters/
pretty.ts
MIT
Before the first npm publish:
npm run check
npm packRecommended final package metadata before publishing:
- Confirm the npm package
nameinpackage.json. - Add
repository,homepage, andbugsURLs inpackage.json. - Publish with
npm publish.
See CONTRIBUTING.md for local setup, validation commands, and the preferred pull request flow.
This repository is intended to be easy to contribute to:
- Standard npm-based setup.
- CI runs the same checks contributors run locally.
- Husky can run checks before commit.
- The codebase stays small and focused so new contributors can understand it quickly.