From problem to open-source: the perfect opportunity to contribute
The day the oxc team opened the alpha for native oxlint plugins, it was the perfect excuse to solve a problem I'd been having at my current job for a while — linting Tailwind CSS classes with oxlint.
If you use Tailwind CSS v4 with oxlint, the existing linting options aren't built for that combo. eslint-plugin-tailwindcss is solid but lives in the ESLint world and its v4 support is still partial. eslint-plugin-better-tailwindcss works in oxlint through the jsPlugins compatibility layer, and it gets the job done — but it's not a native plugin and its rules are more limited. Neither was designed specifically for oxlint + Tailwind CSS v4.
So I built it.
What is oxlint-tailwindcss
A native oxlint plugin with 22 linting rules designed exclusively for Tailwind CSS v4. It's not an ESLint port or a wrapper — it uses the @oxlint/plugins API directly.
This matters because being native means it shares the same parsing cycle as oxlint. There's no interoperability overhead, no translation layer. It's as fast as oxlint itself, and it shows.
Zero configuration
The plugin auto-detects your Tailwind CSS entry point. If your file is called app.css, globals.css, main.css, tailwind.css (or any of the conventional names) and contains @import "tailwindcss", it finds it automatically. No configuration needed.
{
"jsPlugins": ["oxlint-tailwindcss"],
"rules": {
"tailwindcss/no-unknown-classes": "error",
"tailwindcss/no-conflicting-classes": "error",
"tailwindcss/enforce-sort-order": "warn"
}
}
The design system is loaded once and shared across all rules. Efficient.
If you have a non-conventional entry point you can still configure it.
22 rules in four categories
Correctness — Catch real bugs
Correctness rules catch bugs before they reach the browser.
no-unknown-classes detects classes that don't exist in your design system and suggests fixes for typos:
<div className="flex itms-center bg-blu-500" />
// ^^^^^^^^^^^
// "itms-center" is not a valid Tailwind CSS class.
// Did you mean "items-center"?
no-conflicting-classes tells you exactly which CSS property is conflicting and which class wins:
<div className="text-red-500 text-blue-500" />
// "text-red-500" and "text-blue-500" affect "color".
// "text-blue-500" takes precedence (appears later).
no-dark-without-light detects when you use dark: without a base class, something that often causes missing styles in light mode:
// ❌ — what background does it have in light mode?
<div className="dark:bg-gray-900" />
// ✅
<div className="bg-white dark:bg-gray-900" />
no-contradicting-variants catches redundant variants where the base class already applies unconditionally:
// ❌ — dark:flex is redundant, flex already applies always
<div className="flex dark:flex" />
Also included: no-duplicate-classes (with autofix), no-deprecated-classes (with autofix and v4 mapping), and no-unnecessary-whitespace.
Style — Team consistency
enforce-sort-order sorts classes according to the official Tailwind CSS order (with autofix). It has a strict mode that also groups by variant.
enforce-shorthand converts mt-2 mr-2 mb-2 ml-2 to m-2, w-full h-full to size-full, and many more combinations. All with autofix.
enforce-logical converts physical properties to logical ones for LTR/RTL support: ml-4 → ms-4, left-0 → start-0. Its inverse, enforce-physical, does the opposite for projects that are LTR-only and prefer consistency with physical properties. Both with autofix.
enforce-consistent-variable-syntax normalizes CSS variable syntax between bg-[var(--primary)] and v4's shorthand bg-(--primary).
And four more rules: enforce-canonical, enforce-consistent-important-position (default suffix, v4's canonical form), enforce-negative-arbitrary-values, and consistent-variant-order.
Complexity — Keep code manageable
max-class-count warns when an element exceeds 20 classes (configurable). It's the signal that it's time to extract a component.
enforce-consistent-line-wrapping controls the class string length by print width or by number of classes per line.
Restrictions — Design system rules
no-hardcoded-colors forbids hardcoded colors like bg-[#ff5733] in arbitrary brackets — the typical shortcut that erodes your design system.
no-arbitrary-value and no-unnecessary-arbitrary-value (with autofix) control the use of arbitrary values. The latter detects when you use h-[auto] but h-auto exists.
no-restricted-classes allows blocking specific classes by name or regex, with custom messages.
Class extraction
The parser is what makes all of this work reliably. It's not a regex that looks for className= and hopes for the best. It extracts classes from:
- JSX attributes (
className,class) - Template literals with interpolation
- Ternaries
- Utility functions:
cn(),clsx(),cx(),cva(),twMerge(),twJoin(), and more - Full
cva()— base, variants, compoundVariants - Full
tv()— base, slots, variants with slot objects, compoundSlots - Tagged templates (
tw\...``) - Variables by name (
className,classes,style,styles)
It handles nested brackets, nested calc, arbitrary variants, quoted values, important modifier, negative values, and named groups/peers. The edge cases that break other parsers.
The story behind it
I started by planning what I wanted, the stack I was going to use, and how I wanted everything to work. After planning the implementation with Claude Code, the iteration began until reaching the current 22 rules. The repo includes a CLAUDE.md and configured skills that allow any contributor to use the same workflow to write new rules — the same tool the plugin was built with. If you want to add a rule, Claude Code already knows how to do it in this project.
The project runs entirely on the VoidZero tool ecosystem. tsdown for the build, oxfmt for formatting, vitest for testing, tsgo (native TypeScript 7 in Go) for type checking, and of course oxlint for linting the plugin itself. Every tool in the chain is built on Rust or optimized for speed.
It wasn't a cosmetic decision — it was deliberate dogfooding. If you're going to make a plugin for oxlint, it makes sense that the entire toolchain is from the same ecosystem. And if you're going to develop with an AI agent, it makes sense that the repo is prepared for it.
Getting started
pnpm add -D oxlint-tailwindcss
Add the plugin to your .oxlintrc.json:
{
"jsPlugins": ["oxlint-tailwindcss"],
"rules": {
"tailwindcss/no-unknown-classes": "error",
"tailwindcss/no-duplicate-classes": "error",
"tailwindcss/no-conflicting-classes": "error",
"tailwindcss/no-deprecated-classes": "error",
"tailwindcss/no-unnecessary-whitespace": "error",
"tailwindcss/enforce-sort-order": "warn",
"tailwindcss/enforce-shorthand": "warn",
"tailwindcss/no-hardcoded-colors": "warn"
//...
}
}
Run oxlint. That's it.
Try it
The plugin is at v0.1.x — functional, tested, and ready for production use (some companies are already using it). But a linter gets better with real feedback from real projects. If you try it and find a case it doesn't handle well, open an issue. If you want to contribute a rule, the repo is already set up so you can iterate with Claude Code from minute one. And if it was simply useful to you, a star on GitHub helps more people find it.