Del problema al open-source: la oportunidad perfecta para contribuir
El día que el equipo de oxc abrió el alpha de plugins nativos para oxlint, fue la excusa perfecta para solucionar un problema que llevaba tiempo teniendo en mi trabajo actual, lintear las clases de Tailwind CSS con oxlint.
Si usas Tailwind CSS v4 con oxlint, las opciones de linting existentes no están pensadas para ese combo. eslint-plugin-tailwindcss es sólido pero vive en el mundo de ESLint y su soporte de v4 todavía es parcial. eslint-plugin-better-tailwindcss funciona en oxlint a través de la capa de compatibilidad jsPlugins, y hace el trabajo — pero no es un plugin nativo y sus reglas son más acotadas. Ninguno fue diseñado específicamente para oxlint + Tailwind CSS v4.
Así que lo construí.
Qué es oxlint-tailwindcss
Un plugin nativo de oxlint con 22 reglas de linting diseñadas exclusivamente para Tailwind CSS v4. No es un port de ESLint ni un wrapper, usa directamente la API de @oxlint/plugins. Solo 2 dependencias en runtime.
Esto importa porque al ser nativo, comparte el mismo ciclo de parseo que oxlint. No hay overhead de interoperabilidad, no hay capa de traducción. Es tan rápido como oxlint mismo y se nota.
Funciona sin configuración
El plugin auto-detecta tu entry point de Tailwind CSS. Si tu archivo se llama app.css, globals.css, main.css, tailwind.css (o cualquiera de los nombres convencionales) y contiene @import "tailwindcss", lo encuentra solo.
{
"jsPlugins": ["oxlint-tailwindcss"],
"rules": {
"tailwindcss/no-unknown-classes": "error",
"tailwindcss/no-conflicting-classes": "error",
"tailwindcss/enforce-sort-order": "warn"
}
}
La auto-detección sigue @import statements un nivel de profundidad — incluyendo imports de paquetes como @import '@company/theme/tailwind.config.css'. En monorepos, la búsqueda se detiene en boundaries de package.json para que cada paquete resuelva su propio design system automáticamente.
El design system se carga una vez por entry point, con caché en memoria y en disco. En un monorepo con múltiples paquetes que comparten el mismo entry point, el design system se carga solo una vez.
Si tienes un entry point distinto a lo convencional, puedes configurarlo en
settings.tailwindcss.entryPointo por regla.
22 reglas en cuatro categorías
Correctness — Evitar errores reales
Las reglas de correctness atrapan bugs antes de que lleguen al navegador.
no-unknown-classes detecta clases que no existen en tu design system y sugiere correcciones para typos:
<div className="flex itms-center bg-blu-500" />
// ^^^^^^^^^^^
// "itms-center" is not a valid Tailwind CSS class.
// Did you mean "items-center"?
Soporta allowlist para permitir clases custom que no están en tu design system e ignorePrefixes para saltarse prefijos que no son clases de Tailwind.
no-conflicting-classes te dice exactamente qué propiedad CSS está en conflicto y cuál clase gana:
<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 detecta cuando usas dark: sin una clase base, algo que suele causar estilos faltantes en light mode:
// ❌ — ¿qué fondo tiene en light mode?
<div className="dark:bg-gray-900" />
// ✅
<div className="bg-white dark:bg-gray-900" />
no-dark-without-light chequea dark: por defecto, pero la opción variants permite aplicar el mismo patrón a cualquier variante — útil si tu proyecto usa variantes custom.
no-contradicting-variants atrapa variantes redundantes donde la clase base ya aplica incondicionalmente:
// ❌ — dark:flex es redundante, flex ya aplica siempre
<div className="flex dark:flex" />
no-deprecated-classes reemplaza automáticamente las clases deprecadas en v4:
// ❌ v3
<div className="flex-grow overflow-ellipsis decoration-slice" />
// ✅ v4 (autofix)
<div className="grow text-ellipsis box-decoration-slice" />
También flex-shrink → shrink y decoration-clone → box-decoration-clone.
Y las clásicas no-duplicate-classes (con autofix) y no-unnecessary-whitespace.
Style — Consistencia del equipo
enforce-sort-order ordena las clases según el orden oficial de Tailwind CSS (con autofix), compatible con oxfmt y prettier-plugin-tailwindcss. Su modo strict agrupa las clases por prefijo de variante, ordena dentro de cada grupo y ordena los grupos por prioridad de variante.
enforce-shorthand convierte mt-2 mr-2 mb-2 ml-2 en m-2, w-full h-full en size-full, y muchas más combinaciones. Todo con autofix.
enforce-logical convierte propiedades físicas en lógicas para soporte LTR/RTL: ml-4 → ms-4, left-0 → start-0. Su inversa, enforce-physical, hace lo contrario para proyectos que son solo LTR y prefieren consistencia con propiedades físicas. Ambas con autofix.
enforce-consistent-variable-syntax normaliza la sintaxis de variables CSS entre bg-[var(--primary)] y la shorthand de v4 bg-(--primary).
enforce-canonical convierte valores arbitrarios a clases nativas cuando existen: p-[2px] → p-0.5 (usa rootFontSize de 16px por defecto para la conversión). Funciona directo con la API de canonicalización de Tailwind.
enforce-consistent-important-position (default suffix, la forma canónica de v4), enforce-negative-arbitrary-values (-top-[5px] → top-[-5px]) y consistent-variant-order completan las reglas de estilo.
Complexity — Mantener el código manejable
max-class-count avisa cuando un elemento supera las 20 clases (configurable). Es la señal de que es hora de extraer un componente.
enforce-consistent-line-wrapping controla el largo del string de clases por print width o por cantidad de clases por línea.
Restrictions — Reglas del design system
no-hardcoded-colors prohíbe colores hardcodeados como bg-[#ff5733] en brackets arbitrarios — el típico atajo que erosiona tu design system.
no-arbitrary-value y no-unnecessary-arbitrary-value (con autofix) controlan el uso de valores arbitrarios. La segunda detecta cuando usas h-[auto] pero existe h-auto.
no-restricted-classes permite bloquear clases específicas por nombre o regex, con mensajes custom.
Extracción de clases
El parser es lo que hace que todo esto funcione de manera confiable. No es un regex que busca className= y reza. Extrae clases de:
- Atributos JSX (
className,class) - Atributos JSX con objetos — e.g. el prop
classNamesde Mantine:<Input classNames={{ root: "flex", input: "border-none" }} /> - Template literals con interpolación
- Ternarios
- Funciones de utilidad:
cn(),clsx(),cx(),cva(),twMerge(),twJoin(), y más cva()completo — base, variants, compoundVariantstv()completo — base, slots, variants con objetos de slots, compoundSlotsclassed()(tw-classed) — ignora el tipo de elemento, extrae clases y config estilo cva- Tagged templates (
tw\...``) - Variables por nombre (
className,classes,style,styles) - Clases de componentes definidas con
@layer components { .btn {} }en tu CSS
Maneja nested brackets, calc anidado, arbitrary variants, quoted values, important modifier, negative values y named groups/peers. Los edge cases que rompen otros parsers.
Detección customizable
Por defecto el plugin detecta clases en atributos comunes, 14 funciones de utilidad, tagged templates tw, y variables con nombres como className/classes/style. Puedes extender estos defaults vía settings.tailwindcss — todos los valores son aditivos:
{
"settings": {
"tailwindcss": {
"attributes": ["overlayClassName"],
"callees": ["myHelper"],
"tags": ["css"],
"variablePatterns": ["^tw"],
},
},
}
Esto aplica a las 22 reglas de una vez. Si necesitas quitar un default built-in, usa exclude en el mismo bloque de settings.
La historia detrás
Partí planificando qué quería, el stack que iba a usar y cómo quería que funcionara todo. Después de planificar la implementación con Claude Code, arrancó la iteración hasta conseguir las 22 reglas actuales. El repo incluye un CLAUDE.md y skills configuradas que permiten a cualquier contribuidor usar el mismo workflow para escribir reglas nuevas — la misma herramienta con la que se construyó el plugin. Si quieres agregar una regla, Claude Code ya sabe cómo hacerlo en este proyecto.
El proyecto corre completamente sobre el ecosistema de herramientas de VoidZero. tsdown para el build, oxfmt para el formateo, vitest para testing, tsgo (TypeScript 7 nativo en Go) para el type checking, y por supuesto oxlint para el linting del propio plugin. Cada herramienta en la cadena está construida sobre Rust u optimizada para velocidad.
No fue una decisión cosmética, fue dogfooding deliberado. Si vas a hacer un plugin para oxlint, tiene sentido que todo el toolchain sea del mismo ecosistema. Y si vas a desarrollar con un agente de IA, tiene sentido que el repo esté preparado para ello.
Cómo empezar
pnpm add -D oxlint-tailwindcss
Agrega el plugin a tu .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"
//...
}
}
Ejecuta oxlint. Eso es todo.
Pruébalo
El plugin es funcional, testeado y usado en producción. Pero un linter se hace mejor con feedback real de proyectos reales. Si lo pruebas y encuentras un caso que no maneja bien, abre un issue. Si quieres contribuir una regla, el repo ya está preparado para que iteres con Claude Code desde el primer minuto. Y si simplemente te resultó útil, una estrella en GitHub ayuda a que más gente lo encuentre.