← CSS: diseño, maquetación y responsive

Sesión 5

Imágenes, estados y movimiento

Tres horas. Media hora para entender cómo encaja una imagen en un hueco que no es el suyo y por qué el estado de foco no es opcional, y dos horas y media dejando el catálogo coherente y el sitio recorrible sin ratón.

Hoy · Hoja de ruta

  1. 1. Aprende: Por qué una imagen rompe un layout, cómo se encajan imágenes de tamaños distintos sin deformarlas, qué estados tiene una interfaz y cuándo un movimiento comunica algo.
  2. 2. Haz: Consigue un catálogo coherente con imágenes irregulares, recorre tu sitio con el teclado y añade solo las transiciones que informen.
  3. 3. Comprueba: Ninguna imagen está deformada, sabes en todo momento dónde está el foco, y cada movimiento de tu página comunica algo.

Antes de empezar · 5 minutos, sin apuntes

  1. ¿Qué significa trabajar mobile first?
  2. ¿Qué diferencia hay entre un layout fluido y una media query?
  3. Ordena el diagnóstico de un desbordamiento horizontal antes de escribir CSS nuevo.

Se explica

25 minutos · conceptos y demostración

Tres asuntos que parecen menores y no lo son: una imagen que no encaja rompe el layout entero, un foco invisible deja la web inservible para quien no usa ratón, y un movimiento sin motivo estorba. Los tres se resuelven con decisiones, no con recetas.

La regla que va en toda hoja de estilos

Una imagen tiene un tamaño propio, y si es más ancha que su contenedor, se sale. Es la causa número uno de scroll horizontal.

img {
    max-width: 100%;
    height: auto;
}

max-width: 100% impide que sobrepase a su contenedor. height: auto es imprescindible junto a la anterior: sin ella, si el HTML declaraba width y height —como pedíamos en la UD1—, al reducirse el ancho la altura se quedaría fija y la imagen se deformaría.

Encajar una imagen en un hueco que no es el suyo

El problema real de un catálogo: las fotos de producto vienen con tamaños y proporciones distintas, y las tarjetas quedan desiguales. La tentación es forzarlas, y eso las estira:

/* Mal: deforma */
.card img { width: 100%; height: 15rem; }

La solución es decirle cómo debe encajar en el hueco que le das:

.card img {
    width: 100%;
    height: 15rem;
    object-fit: cover;
}

object-fit

Cómo se acomoda el contenido de una imagen dentro de la caja que se le ha dado. cover la escala hasta llenarla y recorta lo que sobra, conservando la proporción. contain la encaja entera y deja huecos.

Valor Resultado
fill Por defecto: la estira hasta llenar. Deforma
cover Llena el hueco y recorta. La opción de un catálogo
contain Cabe entera, con huecos a los lados

Si el recorte de cover elimina la parte relevante, object-position determina qué zona se conserva:

.card img { object-fit: cover; object-position: top; }

Mejor todavía que fijar una altura en rem es declarar una proporción, porque entonces la altura se calcula sola a partir del ancho y la tarjeta funciona igual en una columna estrecha que en una ancha:

.card img {
    width: 100%;
    aspect-ratio: 16 / 9;
    object-fit: cover;
}

Una interfaz tiene más de un estado

a              { color: var(--color-primary); }
a:visited      { color: #6d28d9; }
a:hover        { text-decoration: underline; }
a:focus-visible{ outline: 3px solid currentColor; outline-offset: 3px; }
a:active       { color: #b91c1c; }

En formularios, los campos preparados en la UD1 proporcionan ya sus propios selectores:

input:focus     { }
input:required  { }
input:invalid   { }
input:disabled  { }
input:checked   { }

:invalid merece una advertencia: se aplica desde que carga la página, así que un campo obligatorio y vacío aparece en rojo antes de que nadie haya escrito nada. Combínalo con :user-invalid, que solo actúa después de que la persona haya interactuado, o marca el error de otra forma.

Nunca outline: none a secas

El contorno del foco resulta visualmente poco atractivo y constituye la única indicación de posición para quien navega con teclado. Su supresión deja la web inutilizable para esas personas: al pulsar Tab no se produce ningún cambio perceptible.

Si no te gusta el contorno por defecto, sustitúyelo por otro igual de claro: un contorno propio, un cambio de fondo, un borde. Lo que no vale es dejarlo sin ninguna indicación.

:focus-visible

Se aplica cuando el navegador entiende que la indicación de foco hace falta: al llegar con el teclado, sí; al hacer clic con el ratón en un botón, normalmente no.

Así se puede tener un foco de teclado bien visible sin que aparezca un recuadro cada vez que alguien pulsa con el ratón, que era la razón por la que la gente lo quitaba.

:focus-visible {
    outline: 3px solid var(--color-primary);
    outline-offset: 3px;
}

outline tiene una ventaja sobre border para esto: no ocupa espacio en el layout, así que nada se mueve al recibir el foco. Y outline-offset lo separa del elemento para que se lea mejor.

Transiciones y transformaciones

Una transición suaviza el paso de un estado a otro. Se declara en el estado normal, no en el :hover, para que funcione en los dos sentidos:

.card {
    transition: transform 150ms ease, box-shadow 150ms ease;
}

.card:hover {
    transform: translateY(-2px);
    box-shadow: 0 8px 20px rgb(0 0 0 / 0.12);
}
transform    qué propiedad se anima
150ms        cuánto dura
ease         cómo acelera

No animes all

transition: all 300ms parece cómodo y anima también cosas que no querías, incluidas propiedades que obligan al navegador a recalcular el layout entero. El resultado es una interfaz que va a tirones sin que se sepa por qué.

Nombra las propiedades. Si puedes elegir, anima transform y opacity: son las dos que el navegador resuelve sin rehacer el layout, y por eso van suaves.

transform: translateY(-2px);
transform: scale(1.03);
transform: rotate(2deg);
transform: translateY(-2px) scale(1.02);   /* se combinan en una sola declaración */

Una transformación no afecta al espacio que ocupa el elemento: se dibuja movido, pero su hueco sigue donde estaba y nada se descoloca alrededor. Por eso mover una tarjeta con transform es seguro y hacerlo con margin-top no.

El movimiento tiene que decir algo

Una interfaz no mejora por tener zooms, rebotes, sombras y degradados. Cada efecto debería responder a una pregunta: ¿qué le está comunicando esto a quien lo ve?

Movimiento Comunica ¿Vale la pena?
Un botón que se aclara al pasar por encima «Esto se puede pulsar» Sí
Una tarjeta que se eleva ligeramente «Esto es interactivo» Sí
Un campo que se marca al recibir el foco «Estás escribiendo aquí» Sí
Un titular que entra rebotando al cargar Nada No

Hay además quien configura su sistema para reducir las animaciones, y no por gusto: el movimiento puede provocar mareo o desorientación. El sistema operativo lo comunica, y CSS puede leerlo.

prefers-reduced-motion

Una media query que no pregunta por el tamaño de la pantalla sino por una preferencia declarada por quien usa el dispositivo. Existen más de esta familia, como prefers-color-scheme.

@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        transition-duration: 0.01ms !important;
        animation-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }
}

Es uno de los poquísimos sitios donde !important está justificado: es una preferencia de la persona, y debe ganar a cualquier estilo del sitio.

Se trabaja

150 minutos · práctica sobre tu propio proyecto

Los dos primeros pasos arreglan lo que está roto: las imágenes y el foco. Los cuatro últimos añaden movimiento, y después quitan el que sobra.

Paso 1 · El catálogo irregular · 35 min

Reúne al menos seis imágenes deliberadamente dispares: alguna vertical, alguna horizontal, alguna muy grande, alguna pequeña. Puedes usar fotos reales de tu tema.

Consigue que el catálogo se vea coherente sin editar los archivos:

  1. Todas las tarjetas con la misma altura de imagen.
  2. Ninguna imagen deformada.
  3. Nada se sale a 320 px.
  4. Comprueba las verticales: si cover les corta la cabeza a las fotos, ajústalo con object-position.
  5. Explica en un comentario por qué has elegido cover o contain.
Estoy atascado · las imágenes se ven aplastadas

Si has dado width y height a la vez sin object-fit, el navegador estira la imagen hasta esa caja. Añade object-fit: cover.

Y si el problema aparece solo al estrechar la ventana, lo más probable es que te falte height: auto junto al max-width: 100%.

Antes de continuar: las seis tarjetas tienen la misma altura de imagen y ninguna foto está estirada ni aplastada.

Paso 2 · Los estados invisibles · 35 min

Suelta el ratón. Recorre tus cuatro páginas con Tab, Shift + Tab y Enter.

Elemento ¿Se ve el foco? ¿Se distingue del estado normal? Corrección
Enlaces de navegación
Enlaces del contenido
Botones
Campos del formulario
Casillas y radios

Después:

  1. Define un :focus-visible propio y coherente para todo el sitio.
  2. Comprueba que los enlaces del texto se distinguen del resto sin depender solo del color: alguien daltónico necesita el subrayado.
  3. Da estilo a :hover en enlaces y tarjetas.
  4. Añade un estado visible a los campos obligatorios usando :required.
Consultar · selectores modernos que simplifican estos estilos

:is() agrupa como la coma, pero funciona dentro de un selector largo, donde la coma obligaría a repetirlo todo:

/* Sin :is() */
main h1, main h2, main h3 { }

/* Con :is() */
main :is(h1, h2, h3) { }

:where() hace lo mismo con especificidad cero, lo cual es muy útil para estilos base que quieras poder sobrescribir sin pelearte:

:where(h1, h2, h3) { margin-block: 0; }   /* cualquier clase lo sobrescribe */

Y :has() selecciona un elemento por lo que contiene, algo que CSS no pudo hacer durante veinte años:

.campo:has(input:invalid) {
    border-color: #b91c1c;
}

Ahí estás dando estilo al contenedor a partir del estado de su hijo. No hace falta que lo domines: basta con que sepas que los selectores también han evolucionado y que muchos problemas que antes pedían JavaScript ya no lo piden.

Paso 3 · Transiciones con una finalidad · 25 min

  1. Añade una transición a tus botones y otra a tus tarjetas o enlaces.
  2. Nombra las propiedades: nada de all.
  3. Asegúrate de que los estados de :hover y :focus-visible son claros y distintos entre sí.
  4. Por cada transición que escribas, anota en un comentario qué comunica. Si no sabes qué escribir, esa transición sobra.

Paso 4 · Respetar la preferencia de movimiento · 10 min

Añade el bloque de prefers-reduced-motion al final de tu hoja y compruébalo de verdad, activando la reducción de movimiento en la configuración de tu sistema o desde DevTools, en el panel de Rendering.

Anota qué cambia en tu sitio al activarla. Si no cambia nada, o el bloque no está bien escrito, o tus transiciones no estaban donde creías.

Paso 5 · Menos es más · 25 min

Ahora la parte determinante: recorre tu sitio y elimina cualquier efecto que no comunique nada.

Efecto Dónde está Qué comunica ¿Se queda?

Es la única tarea de la unidad en la que se puntúa quitar cosas. Cuenta cuántos efectos tenías y cuántos quedan.

Paso 6 · Revisión cruzada sin ratón · 20 min

Intercambia el sitio con un compañero y recorre el suyo usando solo el teclado, sin tocar el ratón en ningún momento.

  1. Anota el primer punto en el que pierdes de vista el foco.
  2. Anota cualquier elemento al que no consigas llegar.
  3. Comprueba si algún estado se distingue únicamente por el color, entrando en DevTools y forzando una simulación de daltonismo desde el panel de Rendering.
  4. Devuelve las tres respuestas con el formato qué / por qué importa / qué probaría.

El autor decide qué acepta. Si rechaza algo, tiene que justificarlo con lo que hace la interfaz, no con lo que le parece más bonito.

Ampliación si has completado el trabajo

Primero termina y comprueba los seis pasos. Los dos retos se resuelven decidiendo, y uno de ellos consiste casi entero en borrar.

Reto 1 · Las imágenes imposibles

Un cliente entrega estas cinco imágenes para un catálogo de tarjetas iguales:

A · panorámica de 3000 × 600 px    (una estantería entera de la tienda)
B · vertical de 800 × 2400 px      (una torre de ordenador de cuerpo entero)
C · cuadrada de 500 × 500 px       (un ratón sobre fondo blanco)
D · diminuta de 120 × 90 px        (un logotipo de fabricante)
E · foto de 4000 × 3000 px         (un teclado, con la marca en la esquina inferior)

Consigue un catálogo coherente. Para cada una, decide y justifica:

  1. cover o contain, y por qué.
  2. Qué object-position necesita, si necesita alguno. La E tiene la información en una esquina: piensa qué pasa al recortarla en un formato apaisado.
  3. La D es más pequeña que el hueco. Explica qué ocurre al ampliarla y qué harías en un encargo real, que no siempre es una decisión de CSS.
  4. La B, recortada a 16 / 9, deja de mostrar el producto. Propón una solución que no sea deformarla ni recortarla.
  5. Escribe una sola regla de CSS que resuelva bien cuatro de las cinco, y di cuál se queda fuera y por qué.

Reto 2 · Auditoría de movimiento en una interfaz ajena

Elige una web con bastante animación: una tienda grande, una web de producto, una landing.

Movimiento que has visto Qué comunica Decorativo o informativo ¿Lo conservarías?

Localiza al menos seis. Después:

  1. Cuenta cuántos son informativos y cuántos decorativos.
  2. Activa la reducción de movimiento en DevTools y recarga. ¿Respeta la web la preferencia, o se mueve igual? Es la comprobación interesante del reto.
  3. Recorre la página con Tab. ¿Se ve el foco? ¿Alguna animación lo persigue o lo oculta?
  4. Escribe en tres líneas qué le quitarías a esa interfaz y qué ganaría con ello.
Objetivo mínimoCatálogo con imágenes irregulares sin deformar, foco visible en todos los elementos y dos transiciones justificadas.
Si lo tienesLa tabla de estados completa, el bloque de movimiento reducido comprobado y ningún estado que dependa solo del color.
RetoLas cinco imágenes imposibles resueltas con su justificación, y la auditoría de movimiento de una interfaz ajena.

Cierre

5 minutos · comprobación y recuerdo

Lista de verificación de la sesión

  • Todas tus imágenes tienen max-width: 100% y no se deforman.
  • El foco es visible en todos los elementos interactivos.
  • Los enlaces del texto se distinguen sin depender solo del color.
  • Tus transiciones nombran propiedades concretas, no all.
  • Respetas prefers-reduced-motion, comprobado activándolo.
  • Cada efecto que queda tiene escrito al lado qué comunica.

Antes de cerrar · 3 minutos, sin mirar

  1. ¿Qué par de declaraciones evita que una imagen desborde y se deforme?
  2. ¿Qué diferencia hay entre cover y contain?
  3. ¿Por qué no procede escribir outline: none sin una alternativa que lo sustituya?
  4. ¿Qué hace :focus-visible que no hace :focus?
  5. ¿En qué estado se declara la transition, y por qué?
  6. ¿Por qué mover algo con transform es más seguro que con margin?
Ver respuestas

1 · max-width: 100% y height: auto.

2 · cover llena el hueco recortando lo que sobra; contain mete la imagen entera y deja huecos.

3 · Porque deja sin ninguna indicación a quien navega con teclado. Si se quita, hay que poner otra igual de clara.

4 · Solo aplica el estilo cuando la indicación hace falta, típicamente al llegar con el teclado y no al pulsar con el ratón.

5 · En el estado normal, para que la animación ocurra tanto al entrar como al salir del estado.

6 · Porque transform no cambia el espacio que ocupa el elemento, así que nada se descoloca a su alrededor.

Microprueba semanal 5 · 5–10 minutos

Individual, sin IA y sin apuntes.

  1. Explica cómo evitar que una imagen se deforme dentro de una tarjeta.
  2. Escribe un foco visible que no dependa solo del color.
  3. Añade una transición breve y explica cómo respetarías prefers-reduced-motion.