Selectores y cascada
Lección 07 de 37 · 4 min
Una regla de CSS tiene dos partes: el selector dice a qué elementos apunta y las declaraciones dicen qué cambian. Cuando varias reglas apuntan al mismo elemento, la cascada decide cuál gana.
Tipos de selectores
El selector es lo que va antes de las llaves. Estos son los más comunes:
- Tipo
pTodos los elementos con esa etiqueta. - Clase
.avisoLos elementos que tienen esa clase en su atributoclass. Un elemento puede tener varias. - ID
#notaEl elemento con eseid, que tiene que ser único en la página. - Atributo
[type="email"]Los elementos que tienen ese atributo, o ese valor. - Pseudo-clase
:hover · :first-childUn estado o una posición del elemento.
Escritos juntos, sin espacio, forman un selector compuesto: p.aviso pide un p que además tenga la clase aviso. Los combinadores relacionan elementos entre sí: un espacio elige descendientes a cualquier profundidad, y > solo hijos directos.
p.aviso { } /* un p con la clase aviso */article p { } /* un p en cualquier lugar dentro de un article */article > p { } /* un p que es hijo directo de un article */a:hover { } /* un enlace con el puntero encima */input[type="email"] { } /* un input de tipo email */
Especificidad
Cuando dos reglas le dan valores distintos a la misma propiedad, gana la de mayor especificidad (specificity). La especificidad se escribe como tres números (a, b, c):
- aCuántos IDs tiene el selector.
- bCuántas clases, atributos y pseudo-clases.
- cCuántos tipos y pseudo-elementos. El selector universal
*y los combinadores no suman.
Se compara columna por columna, de izquierda a derecha, y decide la primera que difiere. Por eso (1, 0, 0) le gana a (0, 12, 0): no es un número decimal, y ninguna cantidad de clases alcanza a un ID. Si las tres columnas empatan, gana la regla que aparece después.
Activá y desactivá reglas y mirá cuál gana. Los controles están en el orden en que aparecen en la hoja de estilos; la tabla las ordena como el navegador y marca en naranja la columna que decide.

Dos días en las dunas de Merzouga
El último tramo se hace en camello: llevá agua y protector solar.
| selector | a | b | c | color |
|---|---|---|---|---|
| p.aviso | 0 | 1 | 1 | ganarebeccapurple |
| .aviso | 0 | 1 | 0 | pisadasienna |
| p | 0 | 0 | 1 | pisadaroyalblue |
| :where(#nota) | 0 | 0 | 0 | pisadadimgray |
decide la columna c: 1 > 0
:where(#nota) { color: dimgray; } /* (0, 0, 0) */p { color: royalblue; } /* (0, 0, 1) */p.aviso { color: rebeccapurple; } /* (0, 1, 1), gana */.aviso { color: sienna; } /* (0, 1, 0) */
<!-- gana p.aviso (0, 1, 1): en Tailwind ese color va como una utilidad en el elemento --><p class="text-[rebeccapurple]">El último tramo se hace en camello: llevá agua y protector solar.</p>
:where() tiene especificidad cero, lleve lo que lleve adentro: :where(#nota) pierde incluso contra p. En cambio :is() y :not() toman la especificidad de su argumento más específico: :is(#nota, .aviso) vale (1, 0, 0).
El orden de la cascada
La especificidad no es lo primero que mira el navegador. La cascada compara las declaraciones en este orden, y solo pasa al siguiente criterio si hay empate:
- Origen e importanciaEn declaraciones normales, tus estilos le ganan a los del navegador. Una declaración con
!importantle gana a una normal. - Capas
@layerGana la capa declarada después, y lo que no está en ninguna capa le gana a todas. - EspecificidadLa de la sección anterior. Recién acá entra en juego.
- Orden de apariciónA igual especificidad, gana la última.
Es una versión simplificada: también cuentan el Shadow DOM y el atributo style, que le gana a cualquier selector. Evitá !important: para pisarlo hace falta otro !important. Mantené la especificidad baja con :where() y ordená el código en capas:
@layer base, componentes;@layer componentes {.boton { color: royalblue; } /* (0, 1, 0), gana: su capa va después */}@layer base {#app .boton { color: gray; } /* (1, 1, 0), pero en una capa anterior */:where(nav) a { color: inherit; } /* (0, 0, 1): fácil de pisar */}
El orden de las capas lo fija la primera línea, no el orden en que aparecen los bloques.
En Tailwind
Tailwind es utility-first: cada clase hace una sola cosa y casi todas pesan lo mismo, (0, 1, 0). Como no escribís selectores que compitan entre sí, la especificidad deja de ser un problema. Los selectores siguen existiendo, pero como variantes que se anteponen a la clase. Y en v4 el ! de !important va al final: text-[crimson]!.
| CSS | Tailwind |
|---|---|
| a:hover | hover:text-sky-700 |
| :focus-visible | focus-visible:outline-2 |
| li:first-child · li:nth-child(3) | first:font-bold · nth-3:font-bold |
| label:has(input:checked) | has-checked:bg-sky-50 |
| .card:hover .titulo | group → group-hover:underline |
| input:checked ~ label | peer → peer-checked:font-bold |
| article > p | [&>p]:mt-4 |
<label class="group flex items-center gap-2 has-checked:bg-sky-50"><input type="checkbox" class="peer" /><span class="peer-checked:font-bold group-hover:underline">Recordarme</span></label><ul class="[&>li]:py-1"><li class="first:font-bold nth-3:text-gray-500">Uno</li><li>Dos</li><li>Tres</li></ul>
Selectors and cascade
Lesson 07 of 37 · 4 min
A CSS rule has two parts: the selector says which elements it targets and the declarations say what they change. When several rules target the same element, the cascade decides which one wins.
Kinds of selectors
The selector is what comes before the braces. These are the most common:
- Type
pEvery element with that tag. - Class
.warningThe elements that have that class in theirclassattribute. An element can have several. - ID
#noteThe element with thatid, which must be unique on the page. - Attribute
[type="email"]The elements that have that attribute, or that value. - Pseudo-class
:hover · :first-childA state or a position of the element.
Written together, with no space, they form a compound selector: p.warning asks for a p that also has the class warning. Combinators relate elements to each other: a space picks descendants at any depth, and > only direct children.
p.warning { } /* a p with the class warning */article p { } /* a p anywhere inside an article */article > p { } /* a p that is a direct child of an article */a:hover { } /* a link with the pointer over it */input[type="email"] { } /* an input of type email */
Specificity
When two rules give the same property different values, the one with the higher specificity wins. Specificity is written as three numbers (a, b, c):
- aHow many IDs the selector has.
- bHow many classes, attributes and pseudo-classes.
- cHow many types and pseudo-elements. The universal selector
*and combinators add nothing.
It's compared column by column, left to right, and the first column that differs decides. That's why (1, 0, 0) beats (0, 12, 0): it isn't a decimal number, and no number of classes reaches an ID. If all three columns tie, the rule that comes later wins.
Turn rules on and off and watch which one wins. The controls follow the order of the stylesheet; the table ranks the rules the way the browser does and marks in orange the column that decides.

Two days in the Merzouga dunes
The last stretch is by camel: bring water and sunscreen.
| selector | a | b | c | color |
|---|---|---|---|---|
| p.warning | 0 | 1 | 1 | winsrebeccapurple |
| .warning | 0 | 1 | 0 | overriddensienna |
| p | 0 | 0 | 1 | overriddenroyalblue |
| :where(#note) | 0 | 0 | 0 | overriddendimgray |
column c decides: 1 > 0
:where(#note) { color: dimgray; } /* (0, 0, 0) */p { color: royalblue; } /* (0, 0, 1) */p.warning { color: rebeccapurple; } /* (0, 1, 1), wins */.warning { color: sienna; } /* (0, 1, 0) */
<!-- p.warning (0, 1, 1) wins: in Tailwind that color goes on the element as one utility --><p class="text-[rebeccapurple]">The last stretch is by camel: bring water and sunscreen.</p>
:where() has zero specificity, whatever it holds: :where(#note) loses even to p. :is() and :not(), on the other hand, take the specificity of their most specific argument: :is(#note, .warning) is (1, 0, 0).
The cascade order
Specificity isn't the first thing the browser looks at. The cascade compares declarations in this order, and only moves to the next criterion on a tie:
- Origin and importanceFor normal declarations, your styles beat the browser's. A declaration with
!importantbeats a normal one. - Layers
@layerThe layer declared later wins, and whatever is in no layer beats them all. - SpecificityThe one from the previous section. Only now does it come into play.
- Order of appearanceAt equal specificity, the last one wins.
This is a simplified version: the Shadow DOM and the style attribute also count, and the latter beats any selector. Avoid !important: overriding it takes another !important. Keep specificity low with :where() and organize the code in layers:
@layer base, components;@layer components {.button { color: royalblue; } /* (0, 1, 0), wins: its layer comes later */}@layer base {#app .button { color: gray; } /* (1, 1, 0), but in an earlier layer */:where(nav) a { color: inherit; } /* (0, 0, 1): easy to override */}
The first line sets the order of the layers, not the order in which the blocks appear.
In Tailwind
Tailwind is utility-first: each class does one thing and almost all of them weigh the same, (0, 1, 0). Since you don't write selectors that compete with each other, specificity stops being a problem. Selectors still exist, but as variants you put in front of the class. And in v4 the ! of !important goes at the end: text-[crimson]!.
| CSS | Tailwind |
|---|---|
| a:hover | hover:text-sky-700 |
| :focus-visible | focus-visible:outline-2 |
| li:first-child · li:nth-child(3) | first:font-bold · nth-3:font-bold |
| label:has(input:checked) | has-checked:bg-sky-50 |
| .card:hover .title | group → group-hover:underline |
| input:checked ~ label | peer → peer-checked:font-bold |
| article > p | [&>p]:mt-4 |
<label class="group flex items-center gap-2 has-checked:bg-sky-50"><input type="checkbox" class="peer" /><span class="peer-checked:font-bold group-hover:underline">Remember me</span></label><ul class="[&>li]:py-1"><li class="first:font-bold nth-3:text-gray-500">One</li><li>Two</li><li>Three</li></ul>