Ir al contenido
toolingtailwindcssopen-source

oxlint-tailwindcss: el plugin de linting que Tailwind v4 necesitaba

El plugin de linting más completo para Tailwind CSS, nativo para oxlint. 22 reglas con autofix, zero-config y diseñado desde cero para Tailwind CSS v4.

·
5 min de lectura
leer en inglés

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.

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.

Cero 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. No necesitas configurar nada.

{
  "jsPlugins": ["oxlint-tailwindcss"],
  "rules": {
    "tailwindcss/no-unknown-classes": "error",
    "tailwindcss/no-conflicting-classes": "error",
    "tailwindcss/enforce-sort-order": "warn"
  }
}

El design system se carga una vez y se comparte entre todas las reglas. Eficiente.

Si tienes un entry point distinto a lo convencional igual puedes configurarlo.

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"?

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-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" />

También están no-duplicate-classes (con autofix), no-deprecated-classes (con autofix y mapping de v4), y no-unnecessary-whitespace.

Style — Consistencia del equipo

enforce-sort-order ordena las clases según el orden oficial de Tailwind CSS (con autofix). Tiene un modo strict que además agrupa por 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-4ms-4, left-0start-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).

Y otras cuatro reglas más: enforce-canonical, enforce-consistent-important-position (default suffix, la forma canónica de v4), enforce-negative-arbitrary-values y consistent-variant-order.

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)
  • Template literals con interpolación
  • Ternarios
  • Funciones de utilidad: cn(), clsx(), cx(), cva(), twMerge(), twJoin(), y más
  • cva() completo — base, variants, compoundVariants
  • tv() completo — base, slots, variants con objetos de slots, compoundSlots
  • Tagged templates (tw\...``)
  • Variables por nombre (className, classes, style, styles)

Maneja nested brackets, calc anidado, arbitrary variants, quoted values, important modifier, negative values y named groups/peers. Los edge cases que rompen otros parsers.

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 está en v0.1.x — funcional, testeado y listo para usar en producción (algunas empresas ya lo están usando). 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.


GitHub · npm

Sergio Azócar

Sergio Azócar

Software Engineer

Diseño software y comparto lo que aprendo sobre desarrollo, arquitectura y código AI-friendly.

Social

Hecho con Vue · Nuxt & TailwindCSS