+250 skills, dinamita para tu productividad 🧨Explorar →

CSS moderno: buenas prácticas con la skill good-css

Tabla de contenidos

Le pedí a un agente de IA la misma hoja de estilos dos veces. Misma página de precios, mismo HTML, mismo prompt, mismo modelo.

La primera vez me devolvió 45 colores en hexadecimal, el modo oscuro escrito dos veces y seis :hover sueltos que pueden quedarse pegados en el móvil. La segunda, cero hex, cero media queries de ancho y un solo juego de tokens para los dos temas.

Entre una y otra solo cambió una cosa: le di a leer good-css, una skill con 47 técnicas de CSS moderno que Vojta Holik ha recopilado de gente como Ahmad Shadeed, Kevin Powell o Every Layout.

Te cuento lo que yo cambiaría mañana en cualquier hoja de estilos, con el porqué de cada cosa, cuándo no usarla y pens para tocarlo. Las rarezas las dejo para una tabla al final:

  • Las reglas que conviene aplicar siempre, escribas tú el CSS o lo escriba un agente.
  • Cómo se maqueta sin escaleras de media queries: grid intrínseco, sidebar que se pliega sola y container queries.
  • Color moderno y modo oscuro con OKLCH, color-mix() y light-dark().
  • Estados de formulario sin JavaScript con :has() y :user-invalid.
  • Foco, hover y propiedades lógicas para que tu CSS sea accesible y aguante cualquier idioma.
  • Qué cambió de verdad en el código del agente con la skill, qué no cambió y una checklist para revisar lo que te genere.

¿Qué es good-css y por qué le hace falta a tu agente?

good-css es una skill para agentes de IA que reúne 47 técnicas de CSS moderno en 8 categorías, cada una con el snippet, las reglas para aplicarla bien y su línea de soporte en navegadores. Funciona en Claude Code, Codex, Cursor y cualquier agente que lea el formato Agent Skills. Tiene licencia MIT y, cuando escribo esto, 161 estrellas en GitHub.

La frase que lo resume está en su SKILL.md: una declaración que se adapta sola es mejor que un puñado de breakpoints, y una característica de CSS es mejor que un script.

¿Y para qué necesita un agente que se lo recuerden? Porque ya conoce estas técnicas, pero no las elige por defecto. Un modelo escribe lo que más ha visto, y lo que más ha visto es una década de CSS con hex, @media (min-width: 768px) y outline: none. La skill le cambia el punto de partida.

Si no tienes claro qué es una skill y cómo se carga, lo tienes en la guía de Agent Skills para Claude Code, Codex y Cursor. Para instalar esta:

npx skills@latest add vojtaholik/good-css

El instalador te pregunta para qué agentes la quieres. En Claude Code también puedes añadirla como plugin; elige una sola vía, porque con dos instalaciones el agente lee la skill duplicada.

¿Y luego qué? No tienes que invocarla. Su descripción le dice al agente que la use en cuanto toque estilos, sea CSS a pelo, clases de Tailwind o CSS-in-JS, aunque tú no menciones la palabra CSS. El SKILL.md es corto y trae una tabla que indica qué fichero de referencia leer según la tarea (layout, color, interacción, movimiento…), así que el agente solo carga lo que necesita.

💡 No fija un mínimo de navegadores. Cada técnica trae su línea de soporte y la skill pide al agente que te avise si usa algo que falta en tus navegadores objetivo. Díselo tú en el prompt o en tu AGENTS.md («tiene que funcionar en Safari 16») y el aviso tendrá con qué compararse.

¿Qué reglas de CSS moderno deberías aplicar siempre?

Hay ocho reglas que good-css aplica en todo el CSS que toca, sin leer ningún fichero de referencia. Son las que yo convertiría en hábito aunque no uses ningún agente, porque cada una evita un error que se repite en casi todos los proyectos:

  1. Propiedades lógicas (inline y block) en lugar de left, right, top y bottom. Tu diseño aguanta un idioma que se lee de derecha a izquierda sin reescribirlo.
  2. Colores en oklch(), con none como tono en los grises, y variantes derivadas con color-mix(in oklch, …). Un color base y el resto sale de él.
  3. Tamaños que crecen con la pantalla en un solo token clamp(). Una línea en lugar de tres breakpoints para el mismo font-size.
  4. Todo :hover dentro de @media (hover: hover) and (pointer: fine). En el móvil el hover no se queda pegado.
  5. Foco con :focus-visible y outline. Nunca outline: none, que deja a ciegas a quien navega con teclado.
  6. Un estado :active en todo lo que se pueda pulsar. En una pantalla táctil es la única respuesta que ve la persona.
  7. Las transiciones que mueven o escalan, dentro de prefers-reduced-motion: no-preference, con las propiedades nombradas (nunca all) y sin ease-in, que arranca lento y hace que la interfaz parezca tardar.
  8. overflow: clip para recortar. hidden convierte el elemento en un contenedor de scroll (y eso, por ejemplo, rompe un position: sticky de dentro), así que se queda para lo que desplaza un script.

Tres de ellas caben en un bloque que puedes pegar hoy en tu proyecto:

:focus-visible {
  outline: max(2px, 0.08em) solid currentColor;
  outline-offset: 0.25em;
}

@media (hover: hover) and (pointer: fine) {
  .button:hover { background: var(--primary-hover); }
}

.button:active { transform: scale(0.97); }

La regla del hover es la que más se olvida. En una pantalla táctil, un toque activa :hover y lo deja pegado hasta que tocas otra cosa. La query (hover: hover) and (pointer: fine) limita esos estilos a ratones y trackpads. Ojo si usas Tailwind v4: su variante hover: ya se envuelve en (hover: hover), pero no en (pointer: fine).

La séptima regla cambia una costumbre muy extendida: animar todo y luego apagarlo con un bloque prefers-reduced-motion: reduce que pone las duraciones a cero con !important. La skill lo prohíbe. Primero construyes el cambio de estado y después metes el movimiento dentro de la query no-preference. Quien pidió menos movimiento nunca lo recibe.

🔑 Estas ocho reglas son justo el tipo de instrucción que tiene sentido dejar escrita para el agente: cortas, verificables y válidas para todo el proyecto.

Curso en vídeo

Las reglas que tu agente debe seguir siempre, escritas una vez para todo el proyecto

En la lección 3 escribimos el AGENTS.md de un proyecto real regla a regla y vemos por qué esas reglas van ahí y no en la memoria del agente. Tus ocho reglas de CSS encajan en ese mismo fichero.

Entra en el curso gratis →

¿Cómo se maqueta sin escaleras de media queries?

Con layout intrínseco: rejillas y flex que deciden cuántas columnas caben según el espacio disponible, en lugar de que tú fijes un número de columnas por breakpoint. Te quita de encima los umbrales mágicos (52rem, 768px) que nadie recuerda por qué están ahí.

La pieza base es el grid intrínseco, que sustituye a tres o cuatro media queries:

.grid {
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
}

Cada tarjeta mide como mínimo 16rem y el navegador mete tantas columnas como quepan. El min(100%, 16rem) evita que una tarjeta desborde en pantallas más estrechas que 16rem. Si la lista cambia de tamaño (por ejemplo, porque el usuario filtra), usa auto-fill en lugar de auto-fit para que las tarjetas mantengan su ancho y no se estiren.

¿Cuándo no te sirve? Cuando el diseño exige un número exacto de columnas, como tres planes de precio que tienen que ir siempre en fila en escritorio. Ahí decides tú las columnas y el grid intrínseco solo te estorba.

La segunda pieza es la sidebar que se pliega sola, el patrón “The Sidebar” de Every Layout. Dos hijos en un flex con flex-wrap, la barra lateral con un flex-basis de 20rem y el contenido principal con flex-grow: 999 y min-inline-size: 50%. Cuando el principal bajaría del 50% del contenedor, la barra salta debajo. Sin media query.

La tercera son los tamaños fluidos con clamp(): un mínimo, un valor ideal que crece con la pantalla y un máximo, en una sola línea. El fallo típico es poner solo vw en el valor del medio. Un tamaño en vw no crece cuando el lector sube el zoom o el tamaño de letra del navegador, así que escríbelo como rem + vw (por ejemplo, clamp(1rem, 0.9rem + 0.5vw, 1.25rem)), con los límites en rem y un máximo que no pase de 2,5 veces el mínimo para no incumplir el criterio de zoom de WCAG 1.4.4.

Las tres técnicas juntas, a dos anchos de viewport y sin una sola @media:

El mismo HTML y CSS sin media queries a 360 y 760 píxeles: a 360 las tarjetas y la barra lateral se apilan, a 760 las tarjetas van en dos columnas y la barra lateral se pone al lado

Cuando las tarjetas de una fila tienen partes que deben alinearse (título, cuerpo, pie), el remate es subgrid. Cada tarjeta ocupa tres filas del grid padre con grid-row: span 3 y hereda esas filas con grid-template-rows: subgrid. Los pies quedan a la misma altura aunque un título ocupe dos líneas y otro una. Lleva en todos los navegadores desde septiembre de 2023.

Tres tarjetas antes y después de subgrid: arriba los pies quedan a alturas distintas, abajo título, cuerpo y pie están alineados entre las tres tarjetas

Tiene un coste que verás en el experimento: subgrid reparte la altura de la parte más alta a todas, así que una tarjeta con título corto se queda con hueco debajo. A veces compensa y a veces no.

🛡️ Una media query de ancho sigue siendo la herramienta correcta para lo que depende de la pantalla y no de un hueco, como la cabecera global del sitio. La skill no dice lo contrario: lo que evita es usar breakpoints donde una declaración se adapta sola.

¿Cuándo usar container queries en lugar de media queries?

Usa container queries en cualquier componente que aparezca en huecos de distinto ancho: una tarjeta que vive en la columna principal y en la barra lateral, un widget que metes en un modal, un bloque que el CMS coloca donde quiere. Una media query solo conoce el viewport. Una container query conoce el espacio que de verdad le han dado al componente.

🔑 El modelo mental que conviene quedarse: el componente decide por su contenedor, no por la pantalla. La página reparte huecos con su layout y cada componente se adapta al hueco que le toca.

Funcionan en todos los navegadores principales desde febrero de 2023, así que ya no hay excusa de soporte. El patrón tiene dos piezas:

.slot { container-type: inline-size; }

@container (width > 30rem) {
  .card { display: grid; grid-template-columns: 12rem 1fr; }
}

El hueco declara que se puede consultar su ancho, y la tarjeta cambia de forma cuando ese hueco supera 30rem. Además tienes las unidades de contenedor: cqi es el 1% del ancho del contenedor. Un título con font-size: clamp(1.25rem, 1rem + 2cqi, 2rem) crece con su hueco, no con la ventana.

En este pen puedes estirar la caja punteada desde la esquina y ver cómo la tarjeta cambia de forma en directo. Debajo está el mismo componente en una barra lateral de 17rem y en una columna ancha, con el mismo HTML y las mismas clases:

Abrir en CodePen la demo de container queries

Las reglas que evitan los tropiezos típicos:

  • El contenedor tiene que ser un ancestro. Un elemento no puede consultarse a sí mismo.
  • Nunca pongas container-type en un elemento que toma su ancho del contenido (un inline-block, un item flex sin base). El contenedor colapsa a cero.
  • Sin contenedor encima, la query nunca casa. Kevin Powell declara header, main y footer como contenedores en su reset, y así la mayoría de componentes no necesitan uno propio.

¿Y cuándo no merece la pena? Si el componente solo vive en un sitio, el grid intrínseco o una media query te bastan. Las container queries se pagan solas cuando reutilizas la pieza en huecos distintos y quieres quitarte de encima clases como .card--sidebar o .card--compact.

Te adelanto una cosa del experimento: la versión con la skill declaró contenedores y usó cqi, pero no escribió ni una @container. Fue la versión sin skill la que adaptó la tarjeta a la barra lateral. Si un componente tiene que cambiar de forma según su hueco, dilo en el prompt con esas palabras.

Las container queries son de esas cosas que el agente conoce y no usa si nadie se lo pide. Cada domingo compartimos con +7.200 developers lo que vamos aprendiendo al programar con IA, también lo que se le escapa.

Quiero esa dinamita 🧨

¿Por qué escribir los colores en OKLCH?

Porque en OKLCH la luminosidad, el croma y el tono son tres números independientes, y eso te deja derivar el hover, el tinte o la versión transparente de un color base sin inventar un segundo hex. oklch() y color-mix() son Baseline desde mayo de 2023.

La diferencia con HSL está en la L. En HSL, un amarillo y un azul al 50% de luminosidad no se ven igual de claros; en OKLCH, dos colores con la misma L sí. Por eso oscurecer un 15% da un resultado parecido en cualquier color de tu paleta.

En la práctica, un color de marca y todo lo demás derivado:

:root {
  --primary: oklch(55% 0.15 250);
  --primary-hover: color-mix(in oklch, var(--primary), black 15%);
  --primary-subtle: color-mix(in oklch, var(--primary) 12%, transparent);
}

Cambias --primary y el hover y el tinte se recalculan solos. Compáralo con la versión sin skill del experimento: tres hex a mano por color de acento (--accent, --accent-hover, --accent-active), y otros tres para el modo oscuro.

¿Y si tu paleta viene de Figma en hex? No hace falta convertirla entera. color-mix(in oklch, #e56a54, black 15%) acepta el hex tal cual y hace la mezcla en OKLCH. Lo que te ahorras es el segundo y el tercer hex escritos a mano.

Un aviso si escribes grises en OKLCH: ponles none como tono (oklch(1 0 none)), no 0. Un 0 es un tono rojo y desvía cualquier mezcla del gris con tu color de marca: con un azul, el resultado sale lila.

¿Cómo se hace el modo oscuro con light-dark()?

Con light-dark() cada token lleva sus dos valores, el claro y el oscuro, y para cambiar de tema solo cambias la propiedad color-scheme. Ningún token se redefine bajo una media query ni bajo una clase .dark. Es Baseline desde mayo de 2024 (Chrome 123, Firefox 120, Safari 17.5).

:root {
  color-scheme: light dark;
  --background: light-dark(oklch(1 0 none), oklch(0.145 0 none));
  --foreground: light-dark(oklch(0.145 0 none), oklch(0.985 0 none));
}

[data-theme="dark"] { color-scheme: dark; }

Con color-scheme: light dark el navegador sigue la preferencia del sistema. Si el usuario fuerza un tema con tu botón, pones color-scheme: light o dark en ese elemento y ya está. Y funciona por elemento: una sección con data-theme="dark" dentro de una página clara resuelve sus tokens en oscuro.

¿Cuándo no? Si tienes que dar soporte a navegadores anteriores a Safari 17.5 o Chrome 123, light-dark() no existe y esos tokens se quedan sin valor. Ahí el bloque bajo prefers-color-scheme sigue siendo la opción segura.

¿Qué te ahorra? En el experimento, la versión sin skill escribió los mismos 17 tokens oscuros dos veces: una para prefers-color-scheme: dark y otra para el tema forzado con data-theme. Dos bloques que hay que mantener sincronizados a mano. La versión con skill tiene un bloque y dos líneas para forzar el tema.

En este pen tienes las dos ideas juntas. Cambia de tema con los botones (funcionan con :has() y sin JavaScript) y mueve el tono del color de marca: fíjate en que el hover y el tinte se recalculan solos en los dos temas sin que toques otro valor.

Abrir en CodePen la demo de light-dark() y OKLCH

Tres detalles para que no te muerda:

  • Añade <meta name="color-scheme" content="light dark"> en el <head> para que el navegador pinte el fondo correcto antes de cargar tu CSS.
  • light-dark() solo acepta colores. Si una imagen o un logo cambian por tema, necesitan su propia regla.
  • Cuidado con el texto sobre colores de marca. Si el fondo del botón no cambia entre temas pero el texto sí, el contraste se rompe en uno de los dos. Por eso el pen usa un token --on-primary que también lleva sus dos valores.

¿Qué estados de formulario resuelve CSS sin JavaScript?

Con :has() y :user-invalid resuelves casi todo lo que antes pedía listeners de blur, clases touched y scripts que añaden clases al padre. Los dos llevan desde finales de 2023 en todos los navegadores principales y en 2026 alcanzaron la categoría de Baseline «ampliamente disponible».

:user-invalid casa con un campo inválido solo después de que la persona haya interactuado con él. A diferencia de :invalid, un formulario recién cargado no aparece lleno de rojo. Y :has() te deja estilizar un padre por lo que contiene:

input:user-invalid { border-color: var(--danger); }
.field:has(input:user-invalid) .error { visibility: visible; }
.plan:has(input:checked) { border-color: var(--primary); }

La primera línea marca el campo. La segunda muestra el texto del error, porque el color solo no basta para comunicar un fallo (la skill lo exige y WCAG también). La tercera resalta entera la tarjeta del plan elegido, no solo el radio.

Ojo con lo que :user-invalid puede saber: solo conoce las reglas nativas del HTML (required, type="email", minlength, pattern). Si la validación depende del servidor, como un email que ya está registrado, sigues necesitando JavaScript para ese mensaje.

Y aquí entra una lección que el experimento me dio gratis. Las dos versiones, con y sin skill, mostraban el mensaje de error con display: block al salir del campo. Ese mensaje empuja el formulario 27px hacia abajo justo cuando haces clic en lo siguiente. Al automatizar la prueba con Playwright, escribir un email mal y hacer clic directo en la opción «Premium» no la seleccionaba en 3 de 4 combinaciones de versión y ancho: el clic empezaba sobre la opción y terminaba fuera, porque el layout se había movido en medio.

El arreglo es reservar el hueco del error desde el principio (visibility: hidden y min-block-size: 1lh) y solo hacerlo visible. La skill no tiene una regla para esto. En el pen tienes un interruptor para comparar los dos comportamientos: escribe un email mal y haz clic en una opción con el interruptor en cada posición.

Abrir en CodePen la demo del formulario con :has() y :user-invalid

:has() da para más que formularios: html:has(dialog:modal) bloquea el scroll de la página mientras hay un modal abierto. Y una regla de la skill que no sobra repetir: el servidor sigue validando. CSS da feedback a la persona, no seguridad.

¿Cómo se hacen un foco y un hover accesibles?

El foco se estiliza con :focus-visible y outline, el hover se limita a dispositivos con puntero fino y todo lo pulsable lleva un :active y un área de toque de al menos 44px. Sin estos detalles, tu interfaz solo funciona bien con ratón.

:focus-visible muestra el anillo cuando el navegador cree que hace falta (navegación con teclado) y no al hacer clic con el ratón, que era la excusa clásica para escribir outline: none. La skill prohíbe ese outline: none. Si un componente pinta su anillo con box-shadow, deja el outline en outline-color: transparent para que el modo de alto contraste de Windows pueda repintarlo.

Para los botones pequeños, como un aspa de cerrar de 24px, la técnica del área de toque mayor que lo que se ve amplía la zona pulsable con un pseudoelemento:

.icon-button { position: relative; }
.icon-button::after { content: ""; position: absolute; inset: min(0px, (100% - 44px) / 2); }

El pseudoelemento es invisible y se sale 10px por cada lado de un botón de 24px, así que el dedo acierta en un área de 44×44 sin que el botón crezca en el diseño. En la captura, fíjate en dos cosas: el botón con outline: none no cambia al recibir el foco con el teclado, y la zona pulsable pintada sobre el aspa es mucho mayor que el icono. (El tercer panel, los radios concéntricos, es un detalle de esquinas redondeadas que aquí no aplica: en Web Reactiva todo va con esquinas rectas.)

Tres paneles: un botón con outline none que no cambia al recibir el foco frente a uno con anillo de focus-visible, un botón de cerrar de 24 píxeles con su área táctil de 44 píxeles marcada, y radios concéntricos

¿Por qué usar propiedades lógicas en lugar de left y right?

Porque padding-inline, margin-block-end o inset-inline-end siguen la dirección del texto, y tu diseño se adapta solo a un idioma que se escribe de derecha a izquierda o en vertical. Funcionan en todos los navegadores desde 2021.

inline es el eje en el que avanza el texto (horizontal en español) y block, el eje en el que se apilan los bloques. start y end sustituyen a left/right y top/bottom.

.card { padding-inline: 1.5rem; margin-block-end: 2rem; border-inline-start: 4px solid; }

Un detalle que pilla a mucha gente: los shorthands de cuatro valores (margin: 1rem 2rem, padding, inset) siguen siendo físicos. Para que sean lógicos usa las versiones -inline y -block.

¿Y si trabajas con Tailwind? La skill lo contempla: Tailwind v4 tiene utilidades lógicas como mbs-, pe- o inset-bs-, y pide no escribir nunca mt-, pr- ni top-. Si estás decidiendo entre sistemas de estilos para trabajar con agentes, el debate de fondo lo tienes en StyleX frente a Tailwind cuando programa la IA. good-css no compite con ninguno de los dos: Tailwind o StyleX deciden la sintaxis y la skill decide qué declaraciones escribir.

¿Y si nunca vas a traducir tu web al árabe? Te sigue importando: el código dice lo que quieres (“espacio al final del bloque”) en lugar de cómo se pinta hoy (“margen inferior”).

Si te ha servido repasar foco, hover y propiedades lógicas, cada domingo seleccionamos 12 recursos para developers que programan con IA: herramientas, plantillas y lo que vamos probando. Gratis, desde 2018.

Apúntate gratis →

¿Qué cambia cuando un agente escribe CSS con y sin la skill?

En nuestro experimento cambió casi todo lo que la skill trata y nada de lo que no trata. El agente pasó de 45 hex y 12 rgb() a cero, de dos bloques de tema duplicados a uno, de seis :hover sueltos a ninguno, y de dos media queries de ancho a cero. Pero no escribió ninguna @container y no evitó el salto del formulario.

El montaje:

  • El mismo HTML fijo: una página de precios con tres planes (uno con un nombre larguísimo), la misma tarjeta reutilizada en una barra lateral, modo claro y oscuro y un formulario de alta. El agente solo podía escribir CSS.
  • El mismo prompt, que describe necesidades y no nombra ninguna técnica.
  • El mismo modelo (Claude Sonnet, un solo turno, sin herramientas). A la versión con skill le pasé el SKILL.md y las seis referencias que su tabla indica para esta tarea.
  • El CSS no se tocó después. Lo tienes tal cual en los pens.

Es n = 1 por versión: una anécdota bien documentada, no un benchmark. Un piloto anterior con otro encargo dio la misma tendencia.

Esta es la versión sin la skill. Pulsa «Tema», pasa el ratón por las tarjetas y prueba el formulario:

Abrir en CodePen la versión sin la skill

Es un buen CSS: usa :user-invalid, :has(), :focus-visible, un grid intrínseco y container queries. Fallan los defaults: hex por todas partes, el tema oscuro escrito dos veces y el movimiento apagado con un transition: none !important global.

Y esta es la versión con good-css, con el mismo HTML:

Abrir en CodePen la versión con la skill

Lado a lado, a 1.200px y en claro. Fíjate en los precios: con la skill quedan alineados por subgrid, a costa de un hueco grande bajo los títulos cortos.

Las dos versiones de la página de precios a 1200 píxeles en tema claro: a la izquierda sin la skill, a la derecha con good-css, con los precios alineados entre tarjetas por subgrid

Y en móvil, a 390px y en oscuro, después de escribir un email incorrecto y elegir Premium. Las dos pasan a una columna: la de la izquierda con una media query y la de la derecha con la sidebar que se pliega sola.

Las dos versiones a 390 píxeles en tema oscuro con el formulario relleno: email marcado como erróneo y plan Premium elegido en ambas

Los números, medidos sobre el texto del CSS y con el CSSOM de Chromium:

Métrica Sin skill Con skill
Líneas no vacías 485 368
Colores hex / rgb() 45 / 12 0 / 0
oklch() / light-dark() / color-mix() 0 / 0 / 0 20 / 11 / 7
Bloques de tokens de tema duplicados 2 0
Media queries de ancho 2 0
:hover fuera de la query de hover 6 0
Movimiento reducido Apagado global con !important Opt-in con no-preference
Propiedades físicas / lógicas escritas a mano 25 / 2 4 / 18
@container 1 0
subgrid No Sí

Las 4 propiedades físicas de la versión con skill vienen de su propio reset. La llamada costó 0,15 dólares sin skill y 0,28 con ella, por unos 45 KB más de contexto. En Claude Code el sobrecoste baja, porque el agente solo lee las referencias que necesita.

🔑 La skill cambia el default, no el conocimiento. El modelo ya sabía qué era :user-invalid o una container query. Lo que cambió es lo que elige cuando nadie le dice nada.

¿Qué no te va a resolver la skill?

good-css no sustituye a tu criterio de revisión, no cubre la organización de la cascada y no escribe fallbacks. Lo que el experimento dejó a la vista:

  • Declarar contenedores no es usarlos. La versión con skill puso container-type y no consultó ninguno. La tarjeta de la barra lateral no se adapta.
  • El salto del formulario no lo resolvió ninguna de las dos versiones, porque la skill no tiene una regla para reservar el hueco de un mensaje.
  • La versión sin skill ganó en algunos detalles. Añadió un fallback para navegadores sin :user-invalid y áreas táctiles de 44px en todas las opciones del formulario. La versión con skill solo las tenía en el botón de tema.
  • Subgrid alinea, pero reparte huecos. La skill no te dice cuándo ese trade-off visual no compensa.

Ninguna de sus 47 entradas trata cascade layers (@layer), anidamiento, @scope, metodologías como BEM o CUBE, ni rendimiento de CSS. Es una skill de técnicas, no de arquitectura: si la especificidad se te va de las manos, aquí no está la respuesta.

Tampoco sabe nada de tu proyecto. En Web Reactiva todo va con esquinas rectas y los colores de marca no cambian entre temas; eso no lo adivina ningún agente. Lo que sí puedes hacer es escribirlo una vez en una skill propia que se cargue junto a good-css.

Lleva tus skills al siguiente nivel

Escribe la skill con las reglas de CSS de tu proyecto

good-css trae las técnicas, pero tus tokens, tu paleta y tus decisiones de diseño solo las conoces tú. Te llevas el método para convertirlas en un SKILL.md que tu agente carga solo cuando toca estilos.

Abrir la guía →

Guía premium · Funciona en Claude Code, Codex, Cursor y 25+ agentes

La última pega es el soporte. Muchas entradas no traen fallback y la skill no fija un suelo de navegadores. Si tienes que dar soporte a Safari 15 o a un Firefox ESR antiguo, revisa la línea de soporte de cada técnica. Y abre en el navegador lo que te genere el agente: así salió el fallo del formulario.

¿Qué técnicas de good-css puedes dejar para más adelante?

Las que resuelven casos muy concretos o todavía no funcionan en todos los navegadores. Son interesantes, pero no son lo primero que cambiaría en un proyecto. Las nombro para que sepas que existen:

Técnica Para qué sirve Por qué esperar
Anchor positioning Colocar un menú o un popover junto a su botón sin librería No es Baseline todavía
field-sizing: content Textarea que crece con su texto Baseline desde junio de 2026, aún muy reciente
View transitions entre documentos Fundido entre páginas sin router Firefox no las tiene
interpolate-size Animar la altura de un acordeón <details> Solo Chrome
Carruseles y animaciones ligadas al scroll Flechas, puntos y efectos sin JavaScript Solo Chrome, o Chrome y Safari

El resto son ajustes finos de tipografía y pulido (centrar la etiqueta de un botón sobre sus letras con text-box, iconos que miden lo que las mayúsculas, sombras que no repintan, márgenes para el notch). Si alguna te sirve para un caso concreto, la ficha completa está en el repositorio con su snippet. Las animaciones, de hecho, la skill las delega en las skills de Emil Kowalski, de las que ya hablé al repasar Taste Skill para que tu agente diseñe sin parecer una IA.

¿Cómo empiezo mañana a mejorar mi CSS?

Empieza por lo que más se repite en tu hoja de estilos y no por lo más llamativo. Este es el orden que yo seguiría en un proyecto que ya existe, de más a menos impacto:

  1. Foco y hover. Busca outline: none y outline: 0 y cámbialos por un :focus-visible. Mete los :hover en la query (hover: hover) and (pointer: fine). Son cambios pequeños y los nota cualquiera que use teclado o móvil.
  2. Movimiento opt-in. Si tienes un bloque global que apaga transiciones con !important, sustitúyelo por transiciones dentro de prefers-reduced-motion: no-preference.
  3. Tokens de color con light-dark(). Si tu modo oscuro redefine variables en otro bloque, júntalas. Aprovecha para derivar los hover con color-mix() en lugar de escribir un hex más.
  4. Un componente con container queries. Elige la tarjeta que más se reutiliza y quítale las clases modificadoras por contexto.
  5. Formularios con :user-invalid y :has(). Borra el JavaScript que solo añadía clases, y reserva el hueco de los mensajes de error.
  6. Propiedades lógicas en el código nuevo, sin migrar todo de golpe.

Hay un matiz si aplicas el reset de la skill en un proyecto existente: hazlo regla a regla mirando el resultado, porque min-width: 0 en todos los elementos y las reglas de fuente cambian lo que ya se pinta.

Si trabajas con un agente, instala good-css y pídele cambios por componente, no la web entera de golpe: un diff pequeño se revisa y uno de 500 líneas se acepta a ciegas. Con lo que salió del experimento, esto es lo que yo revisaría cada vez:

  1. Ábrelo en el navegador en claro, en oscuro, a 360px de ancho y recorriéndolo con el tabulador.
  2. Busca en el diff outline: none, hex nuevos, !important y @media (min-width. Si aparecen, pregunta por qué.
  3. Comprueba que los contenedores se consultan. Si ves container-type sin ninguna @container debajo, el componente no se adapta a su hueco.
  4. Rellena mal un formulario y haz clic en lo siguiente. Si algo salta de sitio, pide que reserve el hueco del mensaje.
  5. Escribe en tu AGENTS.md lo que la skill no sabe: tus navegadores objetivo y tus decisiones de diseño, como las esquinas rectas.

Si vienes del CSS de hace unos años, compara con los consejos para mejorar tu CSS de nivel principiante que publicamos en 2021: comentarios, BEM y nomenclatura siguen valiendo, pero el lenguaje ha cambiado mucho. Y para otra capa de buenas prácticas de plataforma, Modern Web Guidance, las skills del equipo de Chrome, cubre accesibilidad y rendimiento desde otro ángulo.

¿Cuál de las seis vas a probar primero?

TL;DR

  • 🎨 good-css es una skill con 47 técnicas de CSS moderno para agentes de IA. No enseña nada que el modelo no sepa: cambia lo que elige por defecto.
  • 📐 Maqueta sin escaleras de breakpoints: grid con auto-fit y minmax(), sidebar que se pliega sola, clamp() con rem + vw y container queries, porque el componente decide por su contenedor y no por la pantalla.
  • 🌗 Un solo juego de tokens con light-dark() y colores en OKLCH, con hover y tintes derivados con color-mix() en lugar de hex escritos a mano.
  • ♿ :focus-visible en lugar de outline: none, hover solo con puntero fino, :active en todo lo pulsable, :user-invalid y :has() para formularios sin JavaScript.
  • 🧪 En el experimento (n = 1) el agente con la skill pasó de 45 hex a 0 y de 2 media queries a 0, pero no usó ninguna @container y no evitó el salto del formulario. Empieza por foco y hover y revisa cada cambio en el navegador, en los dos temas y con el teclado.

Preguntas frecuentes

¿Qué es good-css?

good-css es una skill para agentes de IA creada por Vojta Holik que recopila 47 técnicas de CSS moderno en 8 categorías, de layout y color a interacción y scroll. Cada técnica trae el snippet, sus reglas y su soporte en navegadores. Tiene licencia MIT.

¿Cómo instalo good-css en Claude Code, Codex o Cursor?

Con npx skills@latest add vojtaholik/good-css, que te pregunta para qué agentes instalarla. En Claude Code también puedes usar claude plugin marketplace add vojtaholik/good-css y claude plugin install good-css@good-css. Usa una sola vía para no cargar la skill dos veces.

¿good-css funciona con Tailwind?

Sí. La skill dice qué declaraciones escribir y deja la sintaxis al sistema que ya uses. En Tailwind pide utilidades lógicas como mbs- o pe- en lugar de mt- o pr-, y propiedades arbitrarias cuando no hay utilidad para una técnica.

¿Cuándo debo usar container queries en vez de media queries?

Usa container queries cuando un componente aparece en huecos de distinto ancho, como una tarjeta en la columna principal y en una barra lateral. Usa media queries para lo que depende de la pantalla entera, como la cabecera global.

¿Cómo funciona light-dark() en CSS?

light-dark() recibe dos colores y devuelve el primero si el elemento tiene color-scheme: light y el segundo si tiene color-scheme: dark. Con color-scheme: light dark en :root sigue la preferencia del sistema, y para forzar un tema solo cambias color-scheme. Funciona en Chrome 123, Firefox 120 y Safari 17.5.

¿Tengo que pasar todos mis colores de hex a OKLCH?

No. color-mix(in oklch, …) acepta colores en hex, así que puedes dejar tu paleta base como está y derivar en OKLCH el hover, el tinte o la versión transparente. Lo que te ahorras es escribir a mano un hex distinto para cada variante y cada tema.

¿Cómo reviso el CSS que me escribe un agente?

Ábrelo en el navegador en claro, en oscuro, en una ventana estrecha y con el teclado. Busca en el diff outline: none, hex nuevos, !important y media queries de ancho, y comprueba que cada container-type tiene una @container que lo consulte.

¿Qué diferencia hay entre :invalid y :user-invalid?

:invalid casa con un campo inválido desde que se carga la página, así que un formulario vacío aparece lleno de errores. :user-invalid solo casa cuando la persona ya ha interactuado con el campo o ha intentado enviar el formulario.

¿Por qué no debo usar outline: none?

Porque quita la única pista visual de dónde está el foco para quien navega con teclado. Si el anillo del navegador no te gusta, estilízalo con :focus-visible y outline, o usa outline-color: transparent si lo pintas con box-shadow.

¿good-css habla de cascade layers o de anidamiento de CSS?

No. Ninguna de sus 47 entradas trata @layer, el anidamiento ni @scope. Es una skill de técnicas concretas, no de arquitectura de CSS.

¿Hace que un agente de IA escriba mejor CSS?

En nuestro experimento, con un intento por versión, el agente con la skill eliminó los hex, las media queries de ancho y los hover sueltos, y unificó los tokens de tema. No usó ninguna container query ni evitó un salto de layout en el formulario: sigues teniendo que revisar.

Fuentes

🧨 Última oportunidad para recibir la dinamita que mereces sobre programación con IA el próximo domingo: Suscríbete gratis a Web Reactiva en https://webreactiva.com/newsletter

Imagen de Daniel Primo
Claude, IA de Anthropic

Escrito con la ayuda de la IA generativa de Claude, fuentes fidedignas y con un human in the loop:
Dani Primo.

CEO en pantuflas de Web Reactiva. Programador y formador en tecnologías que cambian el mundo y a las personas. Activo en linkedin, en substack y canal @webreactiva en telegram

12 recursos para developers cada domingo en tu bandeja de entrada

Además de una skill práctica bien explicada, trucos para mejorar tu futuro profesional y una pizquita de humor útil para el resto de la semana. Gratis.