Saltar al contenido

CSS, de a poco

Curso de CSS

Lógica en CSS

Lección 37 de 37 · 5 min

CSS empieza a tomar decisiones que antes pedían JavaScript: elegir un valor según una condición, definir funciones propias, saber qué lugar ocupa un elemento entre sus hermanos y leer números de los atributos del HTML. Casi todo es muy nuevo, así que cada parte dice dónde anda y cómo dejar un respaldo.

if(): un valor según una condición

if() elige un valor adentro de la misma declaración. Se escribe como una lista de condición: valor separadas por punto y coma; gana la primera que se cumple, y else cubre el resto. Hay tres tipos de condición:

  1. Una variablestyle()Pregunta por una propiedad personalizada del mismo elemento, como style(--estado: cancelado).
  2. La pantallamedia()Una media query en línea: media(width < 40rem).
  3. El navegadorsupports()Pregunta si el navegador entiende algo: supports(color: oklch(0 0 0)).
css
.boton {
padding: if(media(width < 40rem): 8px 12px; else: 10px 16px);
background: if(
supports(color: oklch(0 0 0)): oklch(0.65 0.2 38);
else: #e8590c
);
opacity: if(style(--deshabilitado: si): 0.5; else: 1);
}

En el tablero, cada fila tiene --estado en su atributo style y el CSS decide el resto: el texto del estado (con content), su color y si la hora se tacha. La lectura de arriba sale de getComputedStyle: es lo que resolvió el navegador. El relleno usa media(), así que en un celular baja a 6px.

--estado (18:27)
content: … · …

Tablero de salidas de Retiro con cuatro trenes; el de las 18:27 figura como Demorado

Salidas · Retiro, línea Mitre

18:09

HoraDestinoAndénEstado
18:12Tigre1
18:20José León Suárez3
18:27Bartolomé Mitre2
18:35Tigre1
salida.css
.salida > td {
padding-block: if(media(width < 40rem): 6px; else: 10px);
}
.hora {
text-decoration-line: if(style(--estado: cancelado): line-through; else: none);
}
.estado::after {
content: if(
style(--estado: demorado): "Demorado";
style(--estado: cancelado): "Cancelado";
else: "A horario"
);
color: if(style(--estado: demorado): var(--aviso); else: inherit);
}
salida.html
<tr class="[--estado:demorado]">
<td class="[text-decoration-line:if(style(--estado:cancelado):line-through;else:none)]">18:27</td>
<td>Bartolomé Mitre</td>
<td class="after:[content:if(style(--estado:demorado):'Demorado';style(--estado:cancelado):'Cancelado';else:'A_horario')] after:[color:if(style(--estado:demorado):var(--aviso);else:inherit)]"></td>
</tr>

style() mira el propio elemento. Es la diferencia con @container style(), que pregunta por el contenedor: con if() un elemento puede reaccionar a sus propias variables. Conviene cerrar siempre con else, así hay un valor cuando no se cumple ninguna condición.

Todavía no es Baseline: anda en Chrome y Edge desde la versión 137, y Firefox y Safari no lo soportan. Un navegador que no lo entiende descarta la declaración entera, así que alcanza con escribir antes una versión sin if():

css
.hora {
text-decoration-line: none; /* respaldo */
text-decoration-line: if(
style(--estado: cancelado): line-through;
else: none
);
}

@function: funciones propias

@function define una función que devuelve un valor, con el nombre de dos guiones como las variables. Los parámetros pueden tener tipo y valor por defecto, y returns dice qué tipo devuelve. Adentro, result es el valor de salida.

css
@function --escala(--paso <number>, --razon <number>: 1.25) returns <length> {
result: calc(1rem * pow(var(--razon), var(--paso)));
}
h1 { font-size: --escala(3); } /* 1rem × 1.25³ = 31.25px */
h2 { font-size: --escala(2, 1.333); }

El tipo no es decorativo: si le pasás algo que no es un número, la llamada es inválida y la propiedad vuelve a su valor heredado o inicial. Mové la razón: cada línea del espécimen fija solo su --paso, y la función hace la cuenta. En 1.25 la llamada no pasa la razón y usa el valor por defecto.

Guía del Delta · escala tipográfica@function: activa

Delta

Islas del Tigre

Cómo llegar en lancha

Las lanchas colectivas salen de la Estación Fluvial de Tigre.

Horarios de octubre

especimen.css
@function --escala(--paso <number>, --razon <number>: 1.25) returns <length> {
result: calc(1rem * pow(var(--razon), var(--paso)));
}
.especimen > * {
font-size: calc(1rem * pow(1.25, var(--paso))); /* respaldo sin @function */
font-size: --escala(var(--paso)); /* --razon usa su valor por defecto */
}
especimen.html
<div>
<p class="[--paso:3] [font-size:--escala(var(--paso))]">Delta</p>
<p class="[--paso:2] [font-size:--escala(var(--paso))]">Islas del Tigre</p>
</div>

El cuerpo también acepta variables locales y reglas condicionales como @media, para que la función devuelva otra cosa según la pantalla:

css
@function --relleno() returns <length> {
result: 16px;
@media (width < 40rem) {
result: 12px;
}
}

sibling-index() y sibling-count()

sibling-index() devuelve la posición del elemento entre sus hermanos, empezando en 1, y sibling-count() cuántos hermanos hay, contándolo a él. Son enteros, así que entran en calc(). Antes había que escribir esos números a mano en el HTML o calcularlos con JavaScript.

css
/* entrada escalonada: 40ms más por cada elemento */
.item {
transition-delay: calc((sibling-index() - 1) * 40ms);
}
/* repartir: cada uno ocupa una parte igual */
.segmento {
width: calc(100% / sibling-count());
}

En la mesa, cada lugar gira 360deg / sibling-count() veces su posición. Sumá invitados: nadie recalcula nada, la cuenta cambia sola porque cambia la cantidad de hermanos. La entrada se escalona 40ms por lugar; con movimiento reducido, los lugares cambian sin desplazarse.

Mesa 4sibling-count() = 6 · 60° entre lugares
invitados6
lugar.css
.lugar {
--angulo: calc(360deg / sibling-count() * (sibling-index() - 1));
transform: rotate(var(--angulo)) translateY(-110px) rotate(calc(var(--angulo) * -1));
transition: transform 250ms cubic-bezier(0.22, 1, 0.36, 1);
transition-delay: calc((sibling-index() - 1) * 40ms);
animation: aparecer 200ms cubic-bezier(0.22, 1, 0.36, 1) backwards;
animation-delay: calc((sibling-index() - 1) * 40ms);
}
@keyframes aparecer {
from { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
.lugar { transition: none; }
}
/* respaldo: índice y total escritos en el HTML */
@supports not (order: sibling-index()) {
.lugar {
--angulo: calc(360deg / var(--total) * var(--indice));
transition-delay: calc(var(--indice) * 40ms);
animation-delay: calc(var(--indice) * 40ms);
}
}
lugar.html
<ol>
<li class="[--angulo:calc(360deg/sibling-count()*(sibling-index()-1))] [transform:rotate(var(--angulo))_translateY(-110px)_rotate(calc(var(--angulo)*-1))] transition-transform duration-250 ease-[cubic-bezier(0.22,1,0.36,1)] delay-[calc((sibling-index()-1)*40ms)] motion-reduce:transition-none">Ana</li>
…
</ol>

Cuentan todos los hermanos que son elementos del HTML, incluso los ocultos. Por eso la mesa y las marcas naranjas del demo están fuera de la lista: si fueran hermanos de los lugares, cambiarían la cuenta.

Son Baseline 2026 (newly available): Chrome 138, Firefox 154 y Safari 26.2. Para versiones anteriores, el respaldo es el método de siempre: escribir --indice y --total en el HTML y usarlos adentro de @supports not (order: sibling-index()).

attr() con tipo

attr() existe desde siempre, pero solo devolvía texto para content. Ahora puede leer un atributo como número, longitud, color o ángulo, en cualquier propiedad. El tipo va en type(), y después de la coma, el valor si el atributo falta o no es válido.

css
.barra {
--cacao: attr(data-cacao type(<number>), 0);
width: calc(var(--cacao) * 1%);
}
/* con una unidad: data-ancho="240" se lee como 240px */
.foto { width: attr(data-ancho px, auto); }

El HTML solo dice el número (data-cacao="75") y el CSS lo usa dos veces: para el largo de la barra y para el porcentaje de chocolate oscuro en color-mix(). Mové el control: cambia el atributo, no el estilo.

Cata de chocolates · % de cacaowidth: …% · …px
  • Ecuador · Esmeraldas70%
  • Madagascar · Sambirano64%
  • Venezuela · Sur del Lago75%
  • Perú · Piura85%
  • Brasil · Bahía, con leche38%
barra.css
.barra {
width: 0; /* respaldo: sin barra; el porcentaje ya está escrito */
}
@supports (width: attr(data-cacao type(<number>))) {
.barra {
--cacao: attr(data-cacao type(<number>), 0);
width: calc(var(--cacao) * 1%);
background: color-mix(in oklch, #3b2417 calc(var(--cacao) * 1%), #e6cdb0);
}
}
barra.html
<p>Venezuela · Sur del Lago</p>
<div class="w-0 supports-[width:attr(data-cacao_type(<number>))]:w-[calc(attr(data-cacao_type(<number>),0)*1%)] supports-[width:attr(data-cacao_type(<number>))]:bg-[color-mix(in_oklch,#3b2417_calc(attr(data-cacao_type(<number>),0)*1%),#e6cdb0)]" data-cacao="75"></div>

Por seguridad, un valor que viene de attr() no se puede usar para armar una url(). Para todo lo demás, sirve igual que una variable.

En Tailwind

Tailwind no tiene clases para nada de esto: van como propiedades arbitrarias entre corchetes, con guiones bajos en lugar de espacios.

CSSTailwind
--estado: cancelado[--estado:cancelado]
text-decoration-line: if(…)[text-decoration-line:if(…)]
content en un pseudoelementoafter:[content:if(…)]
font-size: --escala(var(--paso))[font-size:--escala(var(--paso))]
transition-delay con sibling-index()delay-[calc((sibling-index()-1)*40ms)]
width con attr() tipadow-[calc(attr(data-cacao_type(<number>),0)*1%)]
tablero.html
<tr class="[--estado:demorado]">
<td class="
[text-decoration-line:if(style(--estado:cancelado):line-through;else:none)]">
18:27
</td>
<td class="
after:[content:if(style(--estado:demorado):'Demorado';else:'A_horario')]">
</td>
</tr>
<li class="delay-[calc((sibling-index()-1)*40ms)] transition-opacity">Ana</li>