twlinter
v0.8.0
Published
CLI for scanning projects and reporting non-canonical Tailwind CSS classes
Maintainers
Readme
twlinter
The Tailwind CSS linter that finds every non-canonical class and tells you exactly what to replace it with.
h-[350px] → h-87.5
text-sm leading-5 → text-sm/5
overflow-hidden text-ellipsis whitespace-nowrap → truncateZero config. Tailwind v3 + v4. CLI + ESLint + Oxlint.
npx twlinter@latest
Watch the full-quality video (MP4)
Why twlinter?
Tailwind has many ways to write the same thing. twlinter finds the non-canonical ones, reports each with a source location, and names the exact replacement — then can apply the fixes in place.
Canonical classes
h-[350px] → h-87.5Classes with a simpler form on the theme scale are rewritten as the canonical utility.
Shorthand
overflow-hidden text-ellipsis whitespace-nowrap → truncateClass lists that collapse to fewer utilities are reported as one suggestion.
Design tokens
bg-[#121212] → bg-background
bg-pink-500 → bg-primaryRaw hex values and built-in palette colors are flagged in favor of the theme tokens the project actually declares.
Unknown classes
rounded-huge → did you mean `rounded-full`?Classes Tailwind cannot generate are reported with a spelling suggestion, so silent no-op CSS does not ship.
Duplicates, conflicts, and magic values
p-4 p-4 → p-4
block hidden → pick one
top-[-5px] → -top-[5px]
w-[13px] → w-3.25Auto-fix
Every fixable finding can be written back to your files:
npx twlinter@latest --fixThe same rules in your editor
Every rule ships as an ESLint/Oxlint plugin, so the checks run in your editor and in CI, not just on the command line. See the plugin.
Compatibility
| Check | Tailwind v3 | Tailwind v4 |
| --- | :---: | :---: |
| Custom rules (11) | ✅ | ✅ |
| no-unknown-classes | ✅ | ✅ |
| cssConflict | ✅ | ✅ |
| suggestCanonicalClasses | — | ✅ |
| shorthand-classes | — | ✅ |
| usedBlocklistedClass | — | ✅ |
twlinter detects the Tailwind version from your installed tailwindcss
package. On v3 it reads tailwind.config.* (or the v3 defaults); on v4 it
reads the CSS entry that imports tailwindcss.
Table of contents
- Quickstart
- Usage
- Commands
- Configuration
- Rules
- How rules run
- Oxlint and ESLint plugin
- Output
- Continuous integration
- For coding agents
- Credits
- Development
Quickstart
Run it from the root of your project:
npx twlinter@latestOnce installed, choose your rules or leave the defaults. Prefer to wire it up by hand? See Usage and the plugin.
Usage
That command scans the current project and prints the default terminal report. For the same report as structured JSON:
npx twlinter@latest --jsonFor problems with a concrete replacement, apply the fixes in place (each file is written atomically):
npx twlinter@latest --fixCommands
| Command | What it does |
| --- | --- |
| twlinter | Scan and print the report. |
| twlinter --json | Same report as JSON. |
| twlinter --fix | Apply machine-applicable fixes, then re-scan. |
| twlinter --doctor | Report the detected project and which rules can run. |
| twlinter --explain | After a scan, list rules that ran, were off, or were skipped. |
| twlinter --rules | List every rule, its capability, and default severity. |
| twlinter --config <path> | Use a specific config file. |
| twlinter --no-config | Ignore any config file and use rule defaults. |
--doctor is the "what's missing" view: it prints the Tailwind version and CSS
entry, how many theme colors were read, and a per-rule status — active, off,
or skipped with the reason. --explain does the same for a real scan, so a
rule that produced nothing tells you whether it ran, was disabled, or was
skipped.
--explain writes to stderr, so machine-readable output stays clean:
twlinter --json --explain > result.json # result.json is valid JSON$ twlinter --doctor
Tailwind v4.3.3 entry: src/index.css
Design system loaded theme colors: 42 dependencies: 5
no-raw-colors text active
no-unknown-classes design-system-or-v3 active
usedBlocklistedClass design-system-or-v3 skipped no blocklist in configConfiguration
twlinter is zero-config: with no config file every rule runs at its default
severity. To change that, add twlinter.config.json, .twlintrc.json,
.twlintrc, or a twlinter key in package.json:
{
"rules": {
"no-magic-spacing": "off",
"no-raw-colors": ["error", { "allow": ["*-amber-100"] }],
"no-unknown-classes": "warn"
}
}A rule accepts "off" | "warn" | "error", false, or
["warn" | "error", options]. Rules you omit keep their default; set "off" to
disable one. The same ids are used by the plugin,
so "no-magic-spacing": "off" and "twlinter/no-magic-spacing": "off" mean the
same thing.
A rule id that does not exist (usually a typo) is reported with a suggestion on stderr, and an invalid severity is called out instead of being ignored:
twlinter: Unknown rule "no-magic-spcing". Did you mean "no-magic-spacing"?
twlinter: Invalid setting for rule "no-raw-colors". Use "off", "warn", "error", false, or ["warn"|"error", options].Rules
twlinter has two groups of rules. Every rule is registered with the same id in both the CLI and the Oxlint/ESLint plugin, and can be turned on or off in either. The custom rules check the file text; the language-service rules need Tailwind's design system, which the plugin can only reach where it can run the CLI as a subprocess (see the plugin notes).
Custom rules
| Rule | What it catches |
| --- | --- |
| no-duplicate-utilities | The same utility appearing more than once in one class list. |
| prefer-truncate-shorthand | overflow-hidden text-ellipsis whitespace-nowrap where truncate would do. |
| no-important-abuse | Stacking many ! important utilities in one class list. |
| no-sr-only-display-conflict | sr-only combined with a display utility that overrides it. |
| consistent-negative-arbitrary-values | top-[-5px] instead of -top-[5px]. |
| require-flex-for-flex-utilities | flex-row/flex-col/flex-wrap without flex or inline-flex to act on. |
| prefer-theme-scale | Arbitrary spacing and font-size values that match the theme scale. |
| no-magic-spacing | Arbitrary spacing values that land off the 4px scale. |
| detect-conflicts-in-template-literals | The same utility across parts of a template literal. |
| prefer-design-tokens | Raw hex colors such as bg-[#121212] instead of a theme token. |
| no-raw-colors | Raw Tailwind palette colors such as bg-pink-500 or fill="#ec4899" instead of a theme token. |
Language-service rules
The CLI also runs Tailwind's language service, which needs the design system:
| Rule | What it catches |
| --- | --- |
| suggestCanonicalClasses | Classes with a simpler canonical form, e.g. w-[350px] → w-87.5. |
| shorthand-classes | Class lists that collapse to fewer utilities, e.g. the truncate set. |
| cssConflict | Conflicting utilities such as block and hidden. |
| usedBlocklistedClass | Classes blocked by the Tailwind configuration. |
| no-unknown-classes | Classes Tailwind cannot generate, such as rounded-huge, with spelling suggestions. |
How rules run
Version support
See the compatibility table for the summary.
- Every custom rule runs on Tailwind v3 and v4.
cssConflictandno-unknown-classesrun on both v3 and v4.suggestCanonicalClasses,shorthand-classes, andusedBlocklistedClassrely on the v4 design system and are skipped on v3.- On v3, the spacing-scale rules only suggest class names that exist on the v3 scale; the plugin keeps v4 behavior unless it delegates to the CLI.
Where classes are found
no-unknown-classes reads classes on elements and in cn-style helpers (cn,
clsx, cx, classnames, twMerge, twJoin, tw). It accepts Tailwind
utilities, @utility names, and plain class selectors from the theme's CSS
import graph, and reports anything else with a spelling suggestion when one is
close.
Theme awareness
The CLI reads the project's declared --color-* tokens and passes them to
no-raw-colors.
- A palette color the theme overrides is allowed, and the message can name the project's colors.
- With a readable theme,
no-raw-colorsalso suggests the nearest theme color, reports undeclared color tokens such asbg-brandwith spelling corrections, and names the nearest token for literal SVG colors. - Undeclared color tokens are reported by
no-raw-colorsrather thanno-unknown-classes, which keeps its suggestion for typos of other utilities. - The plugin has no project context, so it checks the built-in palette only.
Oxlint and ESLint plugin
Every rule is published as an ESLint/Oxlint-compatible plugin rule. Install it
and register it under jsPlugins:
npm install -D twlinter oxlint{
"jsPlugins": ["twlinter"],
"rules": {
"twlinter/no-duplicate-utilities": "error",
"twlinter/no-important-abuse": "warn",
"twlinter/no-unknown-classes": "error"
}
}For ESLint, import the plugin and register it the same way:
import twlinter from "twlinter";
export default [
{
plugins: { twlinter },
rules: {
"twlinter/no-duplicate-utilities": "error",
},
},
];Rule ids match the rule field in the CLI report (no-magic-spacing,
prefer-theme-scale, and so on).
Language-service rules in the plugin
The custom rules run entirely in-process. The language-service rules need the compiled Tailwind design system, which is asynchronous — ESLint/Oxlint rules are synchronous. The plugin bridges this by running the twlinter CLI once per process and answering those rules from its report.
That bridge needs to spawn a subprocess, which some hosts block:
- Node / ESLint: works.
- Oxlint: the embedded runtime blocks
spawnSync, so the language-service rules report nothing from the plugin. Enable them there via the CLI (twlinter --json) or by running twlinter in CI.
Set TWLINTER_DELEGATE=0 to disable the bridge entirely.
no-raw-colors accepts allow and deny class patterns, a custom message
(placeholders {{className}}, {{file}}, {{tokens}}, {{suggestions}}),
scanAllStrings, extra mergeFunctions/variantFunctions, and component
contracts. It is fixable, so oxlint --fix can apply the replacement:
{
"jsPlugins": ["twlinter"],
"rules": {
"twlinter/no-raw-colors": [
"error",
{
"allow": ["*-amber-100"],
"contracts": [{ "pattern": "^Badge$", "allow": ["*-amber-500"] }]
}
]
}
}Output
The default report groups findings and includes their source locations:
⚠ The class `h-[350px]` can be written as `h-87.5`
src/app.tsx:12
Found 1 warning. Scanned 18 files in 45ms.--json prints a summary and the same diagnostics:
{
"summary": {
"matchedFiles": 18,
"scannedFiles": 18,
"elapsedMilliseconds": 45,
"warningCount": 1
},
"diagnostics": [
{
"file": "src/app.tsx",
"line": 12,
"column": 8,
"severity": "warning",
"rule": "suggestCanonicalClasses",
"message": "The class `h-[350px]` can be written as `h-87.5`",
"source": "tw"
}
]
}Continuous integration
Run the CLI in a workflow. It exits with code 1 when it finds issues:
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npx twlinter@latestOr use the bundled action. It runs a pinned twlinter version by default for
reproducible results; pass version: latest to follow releases:
- uses: tinyfroggy/twlint@v1
with:
version: latestFor coding agents
Give your coding agent this prompt:
Read https://github.com/tinyfroggy/twlint/blob/main/SETUP.md
and set up twlinter in this project.Credits
twlinter stands on other people's work.
- The canonical-class, shorthand, blocklist, and CSS-conflict checks run on the
Tailwind CSS language service
(
@tailwindcss/language-service). no-raw-colorsandno-unknown-classesare inspired by @shadcn/lint.- The idea of a deterministic, agent-first, zero-config linter is inspired by React Doctor.
Development
npm install
npm run checkRun the source CLI locally with:
npm run dev
npm run dev -- --jsonBuild the publishable package with npm run build.
Releases are manual, using Changesets: add a changeset in a PR, then run the version and publish steps yourself when you want to ship. See CONTRIBUTING.md.
twlinter is open source under the MIT license.
