biome-plugin-tailwindcss
v1.0.0
Published
Biome GritQL plugin with Tailwind CSS linting rules
Downloads
121
Maintainers
Readme
🧩 biome-plugin-tailwindcss
Tailwind CSS v4 linting rules for Biome — enforce design scale, detect deprecated classes, and suggest shorthands
Features • Installation • Usage • Rules • Contributing
👋 Hello there!
biome-plugin-tailwindcss is a Biome GritQL plugin that adds Tailwind CSS v4 linting rules for JSX class attributes. It helps teams keep their Tailwind usage consistent — enforcing design scale values, catching deprecated v3 classes, and suggesting available shorthands.
Four plugin rules ship today. Class ordering is already built into Biome core as useSortedClasses. Rules that require multi-class analysis or Tailwind config access are documented below as candidates for upstream Biome contributions.
✨ Features
🎯 Plugin Rules (GritQL)
- ⚠
no-arbitrary-value— flags arbitrary CSS values in class attributes (w-[42px],text-[#bada55]) and encourages the use of configured design-scale values - ✖
enforces-negative-arbitrary-values— catches the wrong negative-arbitrary form-top-[5px]and enforces the correcttop-[-5px]syntax - ⚠
migration-from-tailwind-3— detects 22 class patterns renamed or removed in Tailwind CSS v4: all six opacity utilities (bg-opacity-*→bg-black/50), flex shorthands (flex-grow→grow), shadow/blur/rounded scale renames,outline-none→outline-hidden, and more - 💡
enforces-size-shorthand— detectsw-X h-Xpairs with equal values and suggests thesize-Xshorthand introduced in Tailwind v4; covers all 48 default size values with zero false positives
🏗️ Built-in Biome Coverage
- ✅
classnames-order— already built into Biome asuseSortedClassesin thenurserygroup; see the usage section for how to enable it
📋 Rule Coverage
| Rule | Status | Source |
|---|---|---|
| classnames-order | ✅ Biome core (useSortedClasses) | docs |
| no-arbitrary-value | ✅ This plugin | source |
| enforces-negative-arbitrary-values | ✅ This plugin | source |
| migration-from-tailwind-3 | ✅ This plugin | source |
| enforces-size-shorthand | ✅ This plugin | source |
| no-contradicting-classname | 🔧 Needs Biome core PR | — |
| enforces-shorthand | 🔧 Needs Biome core PR | — |
| no-custom-classname | 🔧 Needs Biome core PR | — |
| no-unnecessary-arbitrary-value | 🔧 Needs Biome core PR | — |
📖 Table of Contents
- ✨ Features
- 📦 Installation
- 🚀 Usage
- 📚 Rules
- ❓ Why are some rules missing?
- 📃 Scripts
- 🧪 Testing
- 📁 Project Structure
- 🤝 Contributing
- 📜 License
- 📧 Contact & Support
📦 Installation
npm install --save-dev biome-plugin-tailwindcss
# or
pnpm add --save-dev biome-plugin-tailwindcss
# or
yarn add --save-dev biome-plugin-tailwindcssPeer Dependencies
This plugin requires Biome ≥ 2.0.0. GritQL plugin support was introduced in Biome 2. Install it if you haven't already:
npm install --save-dev @biomejs/biome🚀 Usage
Biome does not currently resolve plugins from node_modules automatically. You must reference .grit files using their explicit node_modules paths.
ℹ️ Note for all examples: the
useSortedClassesnursery rule sorts your Tailwind classes just likeprettier-plugin-tailwindcss. The unsafe fix won't apply on save — runbiome lint --fix --unsafeto apply it, or enable the rule as"error"with a pre-commit hook.
Adding to an existing biome.json
If you already have a biome.json, add a plugins array and extend your existing linter section. Only the highlighted keys are new:
{
"$schema": "https://biomejs.dev/schemas/2.0.0/schema.json",
"plugins": [
"./node_modules/biome-plugin-tailwindcss/rules/no-arbitrary-value.grit",
"./node_modules/biome-plugin-tailwindcss/rules/enforces-negative-arbitrary-values.grit",
"./node_modules/biome-plugin-tailwindcss/rules/migration-from-tailwind-3.grit",
"./node_modules/biome-plugin-tailwindcss/rules/enforces-size-shorthand.grit"
],
"linter": {
"enabled": true,
"rules": {
"nursery": {
"useSortedClasses": {
"level": "warn",
"fix": "unsafe"
}
}
}
}
}The plugins key is merged with any existing rules — it does not replace them. If you already have a rules.nursery block, add useSortedClasses inside it.
Option A — Recommended preset (copy-paste ready)
Copy the contents of presets/recommended.json into your biome.json. It enables all four plugin rules and the built-in useSortedClasses at warn severity.
Option B — Strict preset
Copy presets/strict.json to use the single all.grit bundle (all rules in one file) with useSortedClasses set to error.
{
"$schema": "https://biomejs.dev/schemas/2.0.0/schema.json",
"plugins": [
"./node_modules/biome-plugin-tailwindcss/rules/all.grit"
],
"linter": {
"enabled": true,
"rules": {
"nursery": {
"useSortedClasses": {
"level": "error",
"fix": "unsafe"
}
}
}
}
}Option C — Individual rules
Pick only the rules you need:
{
"plugins": [
"./node_modules/biome-plugin-tailwindcss/rules/no-arbitrary-value.grit"
]
}📚 Rules
⚠ no-arbitrary-value
Flags Tailwind CSS arbitrary values in class and className attributes. Arbitrary values bypass your design scale and make the codebase harder to maintain consistently.
// ✗ Invalid — arbitrary values
<div className="w-[42px] h-[calc(100%-2rem)] text-[#bada55]" />
// ✓ Valid — scale values
<div className="w-10 h-full text-red-500" />Applies to both JSX string attributes and expression containers (including clsx, cx, cva, cn, twMerge).
Severity: warn
✖ enforces-negative-arbitrary-values
Tailwind CSS expects negative arbitrary values in the form property-[-value], not -property-[value]. The latter can cause confusion with double-negative variables and is not supported in all versions.
// ✗ Invalid — negative modifier before property name
<div className="-top-[5px] -left-[10px]" />
// ✓ Valid — negative value inside brackets
<div className="top-[-5px] left-[-10px]" />Note: multi-component utility names with arbitrary values (e.g.
border-t-[4px]) are not flagged — only class tokens that start with a leading-are.
Severity: error
⚠ migration-from-tailwind-3
Detects Tailwind CSS v3 class names that were renamed or removed in Tailwind CSS v4. Run this rule during your v3 → v4 migration to locate all affected usages.
Opacity utilities (removed — use opacity modifier syntax)
| v3 class | v4 replacement |
|---|---|
| bg-opacity-{n} | bg-{color}/{n} e.g. bg-black/50 |
| text-opacity-{n} | text-{color}/{n} e.g. text-white/75 |
| border-opacity-{n} | border-{color}/{n} |
| divide-opacity-{n} | divide-{color}/{n} |
| ring-opacity-{n} | ring-{color}/{n} |
| placeholder-opacity-{n} | placeholder-{color}/{n} |
Renamed utilities
| v3 class | v4 class |
|---|---|
| flex-grow | grow |
| flex-grow-0 | grow-0 |
| flex-shrink | shrink |
| flex-shrink-0 | shrink-0 |
| overflow-ellipsis | text-ellipsis |
| decoration-slice | box-decoration-slice |
| decoration-clone | box-decoration-clone |
| outline-none | outline-hidden |
| shadow | shadow-sm |
| shadow-sm | shadow-xs |
| drop-shadow | drop-shadow-sm |
| drop-shadow-sm | drop-shadow-xs |
| blur | blur-sm |
| blur-sm | blur-xs |
| backdrop-blur | backdrop-blur-sm |
| backdrop-blur-sm | backdrop-blur-xs |
| rounded | rounded-sm |
| rounded-sm | rounded-xs |
// ✗ Invalid — v3 class names
<div className="flex-grow bg-opacity-50 shadow outline-none" />
// ✓ Valid — v4 equivalents
<div className="grow bg-black/50 shadow-sm outline-hidden" />Severity: warn
Tip: After running this rule, use manual review for the shadow/blur/rounded scale renames — the scale shifted, so the correct v4 replacement depends on your intended visual weight.
💡 enforces-size-shorthand
Tailwind CSS v4 introduced the size-{n} utility, which sets both width and height to the same value in a single class. This rule detects when w-X and h-X appear together with an identical value and suggests using size-X instead.
// ✗ Flagged — redundant pair when values are equal
<div className="w-4 h-4 p-2" />
<div className="flex items-center w-full h-full" />
// ✓ Valid — use the size-* shorthand
<div className="size-4 p-2" />
<div className="flex items-center size-full" />Note: the rule only flags pairs where both values are the same (zero false positives).
w-4 h-8is intentionally ignored becausesize-*would not produce the same result.
Covers all 48 default Tailwind v4 size values: numeric scale (0–96) and named values (full, screen, auto, fit, min, max, px, svh, lvh, dvh, and more).
Severity: hint (advisory)
❓ Why are some rules missing?
The four rules listed as 🔧 require capabilities that GritQL does not currently support:
- Multi-class analysis —
no-contradicting-classnameandenforces-shorthandneed to compare multiple class tokens in the same attribute against a property map. GritQL matches one AST node at a time. - Tailwind config access —
no-custom-classnameandno-unnecessary-arbitrary-valueneed to resolve the full class list from your Tailwind theme, which requires executing or statically parsingtailwind.config.js(or@themein v4 CSS files).
These rules must be implemented directly in the Biome repository in Rust, where they are tracked as:
| Rule | Biome issue |
|---|---|
| no-contradicting-classname | noContradictingClasses |
| enforces-shorthand | useShorthandClasses |
| no-custom-classname | noUnknownTwClass |
| no-unnecessary-arbitrary-value | noUnnecessaryArbitraryValue |
Tailwind v4's CSS-first configuration (@theme in .css files) makes this significantly more tractable — Biome can parse CSS natively, which opens the door to reading custom configuration without executing JavaScript.
📃 Scripts
| Script | Command | Description |
|---|---|---|
| test | node tests/run.mjs | Run all rule tests |
| biome:check | biome check . | Check formatting + lint |
| biome:ci | biome ci --reporter=github | CI mode with GitHub annotations |
| biome:fix | biome check --write . | Auto-fix formatting and safe lint fixes |
| biome:format | biome format . | Check formatting only |
| biome:format:fix | biome format --write . | Auto-fix formatting |
| biome:lint | biome lint . | Lint only (no format) |
| biome:lint:fix | biome lint --write . | Auto-fix safe lint issues |
🧪 Testing
The test runner writes a temporary biome.json per test, invokes biome lint, and counts plugin diagnostic lines in the output.
# Install dependencies
npm install
# Run all tests
npm testTest matrix (10 tests):
| Suite | Invalid fixture | Valid fixture |
|---|---|---|
| no-arbitrary-value | triggers ≥ 1 diagnostic | produces 0 diagnostics |
| enforces-negative-arbitrary-values | triggers ≥ 1 diagnostic | produces 0 diagnostics |
| migration-from-tailwind-3 | triggers ≥ 1 diagnostic | produces 0 diagnostics |
| enforces-size-shorthand | triggers ≥ 1 diagnostic | produces 0 diagnostics |
| all.grit (combined) | each invalid triggers ≥ 1 | arbValid + migValid + szValid clean |
📁 Project Structure
biome-plugin-tailwindcss/
├── rules/ # GritQL rule files
│ ├── no-arbitrary-value.grit
│ ├── enforces-negative-arbitrary-values.grit
│ ├── migration-from-tailwind-3.grit
│ ├── enforces-size-shorthand.grit
│ └── all.grit # All rules in a single or {} block
├── presets/ # Ready-to-use biome.json snippets
│ ├── recommended.json # All 4 rules at warn/hint severity
│ └── strict.json # All rules via all.grit at error severity
├── tests/
│ ├── run.mjs # Custom Node.js test runner
│ ├── valid/ # Fixtures that must produce 0 diagnostics
│ │ ├── no-arbitrary-value.jsx
│ │ ├── enforces-negative-arbitrary-values.jsx
│ │ ├── migration-from-tailwind-3.jsx
│ │ └── enforces-size-shorthand.jsx
│ └── invalid/ # Fixtures that must produce ≥ 1 diagnostic
│ ├── no-arbitrary-value.jsx
│ ├── enforces-negative-arbitrary-values.jsx
│ ├── migration-from-tailwind-3.jsx
│ └── enforces-size-shorthand.jsx
├── .github/
│ ├── workflows/
│ │ ├── pr-check.yml # Biome CI + test runner on every PR
│ │ └── publish.yml # semantic-release to npm on main push
│ └── dependabot.yml
├── CLAUDE.md # Claude Code guidance
├── CONTRIBUTING.md # Guide for adding new rules
├── biome.json # Dev linting config
└── package.jsonKey Directories
rules/— One.gritfile per rule plusall.gritwhich combines every rule in a singleor {}block for the strict presetpresets/— Copy-pastebiome.jsonsnippets for common setups; these referencenode_modulespaths so they work withnpm installtests/— Each rule has two fixtures:valid/<rule>.jsx(must lint clean) andinvalid/<rule>.jsx(must produce ≥ 1 plugin diagnostic)
Important Configuration Files
presets/recommended.json— Production-ready config with all four plugin rules anduseSortedClassesenabledpresets/strict.json— Tighter config usingall.gritbundle withuseSortedClassesaterrorCONTRIBUTING.md— GritQL regex tips, the standard wrapper pattern, and the list of rules that belong in Biome core
🤝 Contributing
Contributions are welcome!
- Fork the repository
- Create a feature branch (
git checkout -b feature/new-rule) - Follow the pattern in CONTRIBUTING.md — each rule needs a
.gritfile, two test fixtures, an entry inall.grit, and an update totests/run.mjs - Run tests:
npm test - Open a Pull Request
📜 License
This package is licensed under the MIT License. See the LICENSE file for details.
📧 Contact & Support
Made with ❤️ by the community
If this plugin helped you, please consider giving it a ⭐ on GitHub!
