Skip to content

JanSzewczyk/biome-plugin-tailwindcss

Repository files navigation

🧩 biome-plugin-tailwindcss

npm version npm downloads License: MIT PR Checks Biome ≥ 2.0

Tailwind CSS v4 linting rules for Biome — enforce design scale, detect deprecated classes, and suggest shorthands

FeaturesInstallationUsageRulesContributing


👋 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 correct top-[-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-growgrow), shadow/blur/rounded scale renames, outline-noneoutline-hidden, and more
  • 💡 enforces-size-shorthand — detects w-X h-X pairs with equal values and suggests the size-X shorthand introduced in Tailwind v4; covers all 48 default size values with zero false positives

🏗️ Built-in Biome Coverage

📋 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


📦 Installation

npm install --save-dev biome-plugin-tailwindcss
# or
pnpm add --save-dev biome-plugin-tailwindcss
# or
yarn add --save-dev biome-plugin-tailwindcss

Peer 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 useSortedClasses nursery rule sorts your Tailwind classes just like prettier-plugin-tailwindcss. The unsafe fix won't apply on save — run biome lint --fix --unsafe to 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-8 is intentionally ignored because size-* would not produce the same result.

Covers all 48 default Tailwind v4 size values: numeric scale (096) 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 analysisno-contradicting-classname and enforces-shorthand need to compare multiple class tokens in the same attribute against a property map. GritQL matches one AST node at a time.
  • Tailwind config accessno-custom-classname and no-unnecessary-arbitrary-value need to resolve the full class list from your Tailwind theme, which requires executing or statically parsing tailwind.config.js (or @theme in 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 test

Test 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.json

Key Directories

  • rules/ — One .grit file per rule plus all.grit which combines every rule in a single or {} block for the strict preset
  • presets/ — Copy-paste biome.json snippets for common setups; these reference node_modules paths so they work with npm install
  • tests/ — Each rule has two fixtures: valid/<rule>.jsx (must lint clean) and invalid/<rule>.jsx (must produce ≥ 1 plugin diagnostic)

Important Configuration Files

  • presets/recommended.json — Production-ready config with all four plugin rules and useSortedClasses enabled
  • presets/strict.json — Tighter config using all.grit bundle with useSortedClasses at error
  • CONTRIBUTING.md — GritQL regex tips, the standard wrapper pattern, and the list of rules that belong in Biome core

🤝 Contributing

Contributions are welcome!

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/new-rule)
  3. Follow the pattern in CONTRIBUTING.md — each rule needs a .grit file, two test fixtures, an entry in all.grit, and an update to tests/run.mjs
  4. Run tests: npm test
  5. 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!

⬆ Back to Top

About

Biome GritQL plugin with Tailwind CSS v4 linting rules — enforces design scale usage, detects deprecated classes, and suggests shorthands in JSX.

Topics

Resources

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors