Una transición es el camino entre dos estados. Hasta ahora, cuando una
regla de CSS cambiaba el aspecto de un elemento —un :hover que cambia el
color de fondo—, el cambio era instantáneo: un fotograma con el color
viejo y el siguiente con el nuevo.
Con una transición se le dice al navegador: «no salte ; tarda medio segundo en ir de un valor a otro». El navegador calcula él solo todos los valores intermedios y los va dibujando. No hay que escribir ninguno.
Una transición no es una animación. Una transición solo sabe ir de A a B, y necesita que algo provoque ese cambio. Una animación tiene un guion propio, con las etapas que se quieran, y arranca sola. Eso se verá en el documento siguiente.
Esta es la parte que se olvida. Una transición no hace nada por sí misma: es una instrucción en espera. Hasta que no hay un cambio de estado no se ve absolutamente nada.
Y los estados ya los conocen: son las pseudoclases de la clase anterior, más dos nuevas.
| Pseudoclase | Qué la activa | Quién la activa |
|---|---|---|
:hover | el ratón está encima | el ratón |
:focus | el elemento tiene el foco | el tabulador o un clic dentro |
:active | se está pulsando | el botón del ratón, sin soltar |
:checked | una casilla o un radio está marcado | un clic del usuario |
:target | la dirección de la página acaba en el id de ese elemento | un enlace con # |
Las tres primeras duran lo que dura el gesto. Las dos últimas se quedan:
una casilla marcada sigue marcada, y :target aguanta hasta que se pincha otro
enlace. Eso permite construir cosas que se abren y se quedan abiertas sin una línea de
JavaScript.
Dos reglas: el estado de partida y el de llegada. Y en el de partida, la transición.
transition: background-color 500ms — pasa el ratón por la caja del medio
<div class="row"> <div class="caja">1</div> <div class="caja marcada">2</div> <div class="caja">3</div> </div>
.row { display: flex; gap: 10px; align-items: flex-start; } .caja { width: 90px; height: 70px; background: #dcdcea; border: 2px solid #8686a8; display: flex; align-items: center; justify-content: center; font-family: Verdana, sans-serif; font-size: .8rem; } .marcada { background: #9a9ac8; } .marcada { background: #dcdcea; transition: background-color 500ms; } .marcada:hover { background: #c0392b; }
Y lo mismo sin la línea de transition sería el salto brusco de siempre.
La transición es una sola línea añadida: no cambia qué pasa, cambia
cómo pasa.
No hace falta que el ratón entre en la caja que se mueve. Basta con que el selector llegue
hasta ella: aquí el :hover está en la fila, y lo que se mueve es
la caja del medio.
el ratón entra en la fila, se mueve la caja del medio
<div class="row"> <div class="caja">1</div> <div class="caja marcada">2</div> <div class="caja">3</div> </div>
.row { display: flex; gap: 10px; align-items: flex-start; } .caja { width: 90px; height: 70px; background: #dcdcea; border: 2px solid #8686a8; display: flex; align-items: center; justify-content: center; font-family: Verdana, sans-serif; font-size: .8rem; } .marcada { background: #9a9ac8; } .marcada { transition: transform 400ms; } .row:hover .marcada { transform: translateY(18px); }
El código lleva dos colores, y son los mismos en todos los ejemplos que siguen:
transform: dice
qué cambia. Es el estado de llegada; sin él no hay movimiento ninguno,
con transición o sin ella.transition: dice
que ese cambio se haga poco a poco, y cuánto tarda. Por sí sola no mueve
nada: necesita que haya algo que cambie.Las dos se escriben en reglas distintas, y ahí está lo que despista. El cambio va en la
regla del :hover, porque solo ocurre al pasar el ratón. La transición va en la
regla de reposo, en .marcada, porque el elemento la tiene
siempre: así se aplica tanto al entrar el ratón como al salir. Si se escribiera dentro del
:hover, la caja iría despacio al entrar y volvería de golpe al salir, porque al
salir ya no hay ninguna transición declarada.
El orden en que aparezcan en la hoja no importa; lo que importa es en qué regla está cada una.
Esto es exactamente el mecanismo del menú desplegable: el ratón entra en la opción y lo
que cambia es el submenú. Con el combinador + se puede llegar incluso al hermano
siguiente: .caja:hover + .caja afecta a la caja de al lado.
Una transición se describe con cuatro datos: qué propiedad, cuánto tarda, con qué ritmo y cuándo empieza. Hay una propiedad para cada uno, y una abreviatura que las reúne.
El nombre de la propiedad que debe ir suave. Lo que no esté en esa lista sigue saltando, y eso se puede usar a propósito. Aquí cambian dos cosas con el ratón, pero solo una está en la lista:
transition-property: transform — el giro es suave, el color salta
<div class="row"> <div class="caja">1</div> <div class="caja marcada">2</div> <div class="caja">3</div> </div>
.row { display: flex; gap: 10px; align-items: flex-start; } .caja { width: 90px; height: 70px; background: #dcdcea; border: 2px solid #8686a8; display: flex; align-items: center; justify-content: center; font-family: Verdana, sans-serif; font-size: .8rem; } .marcada { background: #9a9ac8; } .marcada { transition-property: transform; transition-duration: 600ms; } .marcada:hover { transform: rotate(15deg); background: #c0392b; }
Con varias propiedades se separan por comas:
transition-property: transform, background-color; Y con la palabra
all se incluyen todas las que cambien, que es cómodo mientras se aprende pero
obliga al navegador a vigilarlas todas.
El único dato obligatorio: sin duración, la transición vale cero segundos
y no se ve. Se escribe en segundos (0.4s) o en milisegundos
(400ms), que es lo habitual porque se lee mejor.
tres duraciones para el mismo recorrido
<div class="column"> <div class="caja d1">100 ms</div> <div class="caja d2">600 ms</div> <div class="caja d3">2 s</div> </div>
.caja { width: 90px; height: 70px; background: #dcdcea; border: 2px solid #8686a8; display: flex; align-items: center; justify-content: center; font-family: Verdana, sans-serif; font-size: .8rem; } .column { display: flex; flex-direction: column; gap: 10px; } .column .caja { transform: translateX(0); } .d1 { transition: transform 100ms; } .d2 { transition: transform 600ms; } .d3 { transition: transform 2000ms; } .column:hover .caja { transform: translateX(170px); }
El criterio práctico: entre 150 y 400 ms para cambios pequeños (colores, bordes, botones). Por debajo de 100 ms no se percibe como movimiento, y por encima de 600 ms la interfaz se siente lenta y molesta. Los dos segundos del ejemplo son para verlo, no para usarlo.
Un recorrido de 600 ms puede hacerse a velocidad constante, o arrancando despacio, o frenando al final. Eso es la curva, y es lo que separa un movimiento que parece natural de uno que parece mecánico.
las cuatro curvas habituales, el mismo recorrido y la misma duración
<div class="column"> <div class="caja c1">ease</div> <div class="caja c2">linear</div> <div class="caja c3">ease-in</div> <div class="caja c4">ease-out</div> </div>
.caja { width: 90px; height: 70px; background: #dcdcea; border: 2px solid #8686a8; display: flex; align-items: center; justify-content: center; font-family: Verdana, sans-serif; font-size: .8rem; } .column { display: flex; flex-direction: column; gap: 10px; } .column .caja { transition: transform 1200ms; font-size: .7rem; } .column .c1 { transition-timing-function: ease; } .column .c2 { transition-timing-function: linear; } .column .c3 { transition-timing-function: ease-in; } .column .c4 { transition-timing-function: ease-out; } .column:hover .caja { transform: translateX(170px); }
easelinearease-inease-outease-in-outcubic-bezier(...) y steps(...)cubic-bezier define la curva mediante cuatro
números; las cinco anteriores son casos particulares suyos, y es la vía para obtener un
rebote. steps divide el recorrido en saltos discretos.Lo que se espera antes de arrancar. Con el mismo valor en todas no sirve de gran cosa; su uso de verdad es escalonar varios elementos para que entren uno detrás de otro.
la misma transición, tres retardos distintos
<div class="column"> <div class="caja r1">0 ms</div> <div class="caja r2">150 ms</div> <div class="caja r3">300 ms</div> </div>
.caja { width: 90px; height: 70px; background: #dcdcea; border: 2px solid #8686a8; display: flex; align-items: center; justify-content: center; font-family: Verdana, sans-serif; font-size: .8rem; } .column { display: flex; flex-direction: column; gap: 10px; } .column .caja { transition: transform 400ms ease-out; } .column .r1 { transition-delay: 0ms; } .column .r2 { transition-delay: 150ms; } .column .r3 { transition-delay: 300ms; } .column:hover .caja { transform: translateX(170px); }
Un retardo negativo es legal y significa «empieza ya, pero por la
mitad»: transition-delay: -200ms en una transición de 400 ms la arranca a
medio camino.
Las cuatro en una sola línea. El orden de los dos tiempos es lo único que hay que recordar: primero la duración, después el retardo.
transition: <propiedad> <duración> <curva> <retardo>; transition: transform 400ms ease-out 100ms;
Se pueden omitir la curva y el retardo, y queda transition: transform 400ms;,
que es la forma que se usa el 90 % de las veces. Para varias propiedades con tiempos
distintos, se separan por comas:
dos propiedades con tiempos distintos en la misma declaración
<div class="row"> <div class="caja">1</div> <div class="caja marcada">2</div> <div class="caja">3</div> </div>
.row { display: flex; gap: 10px; align-items: flex-start; } .caja { width: 90px; height: 70px; background: #dcdcea; border: 2px solid #8686a8; display: flex; align-items: center; justify-content: center; font-family: Verdana, sans-serif; font-size: .8rem; } .marcada { background: #9a9ac8; } .marcada { transition: transform 250ms ease-out, background-color 800ms linear; } .marcada:hover { transform: scale(1.3); background: #c0392b; }
La caja crece enseguida y el color va llegando después. Cada propiedad lleva su propio juego de cuatro datos.
La transición va en el estado normal, no dentro del :hover.
Es el fallo más repetido del tema, y lo peor es que parece funcionar. En el ejemplo
están las dos versiones: pasa el ratón por cada caja y, sobre todo,
sácalo.
la de la izquierda es correcta; la de la derecha vuelve de golpe
<div class="row"> <div class="caja bien">bien</div> <div class="caja mal">mal</div> </div>
.row { display: flex; gap: 10px; align-items: flex-start; } .caja { width: 90px; height: 70px; background: #dcdcea; border: 2px solid #8686a8; display: flex; align-items: center; justify-content: center; font-family: Verdana, sans-serif; font-size: .8rem; } .bien { background: #dcdcea; transition: background-color 600ms; } .bien:hover { background: #c0392b; } .mal { background: #dcdcea; } .mal:hover { background: #c0392b; transition: background-color 600ms; }
La razón es que la regla de :hover solo existe mientras el ratón
está encima. Al entrar, hay transición. Al salir, el :hover desaparece y
con él la instrucción: no queda nadie que le diga al navegador que suavice el regreso. Puesta
en el estado normal, la instrucción está siempre, así que vale para la ida y para la
vuelta.
La excepción es cuando se busca justamente eso: entrada suave y salida instantánea. Pero entonces es una decisión, no un descuido.
El mismo mecanismo, sin ratón. Aquí el foco marca el campo activo con un borde y un halo, y
sustituye el recuadro del navegador por otra señal igual de visible: quitar
outline sin poner nada en su lugar deja la página inservible para quien navega
con el teclado.
transition en :focus — pincha en el campo o llega con el tabulador
<input type="text" class="campo" placeholder="Escribe aquí">
.campo { padding: 8px 10px; font-family: inherit; border: 2px solid #8686a8; background: #fff; transition: border-color 250ms, box-shadow 250ms; } .campo:focus { outline: none; border-color: #c0392b; box-shadow: 0 0 0 4px #f4cfca; }
Una casilla marcada sigue marcada cuando el ratón se va. Eso convierte a
:checked en un interruptor de verdad, y permite abrir y cerrar cosas sin
JavaScript. La dificultad está en el selector: hay que alcanzar desde la casilla el elemento
afectado, y para eso sirve ~, que designa a un hermano posterior.
marca y desmarca la casilla
<input type="checkbox" id="sw"> <label for="sw">Encender</label> <div class="row"> <div class="caja luz">luz</div> </div>
.row { display: flex; gap: 10px; align-items: flex-start; } .caja { width: 90px; height: 70px; background: #dcdcea; border: 2px solid #8686a8; display: flex; align-items: center; justify-content: center; font-family: Verdana, sans-serif; font-size: .8rem; } #sw { transform: scale(1.3); margin-bottom: 10px; } .luz { transition: background-color 500ms, transform 500ms; } #sw:checked ~ .row .luz { background: #f0d040; transform: scale(1.15); }
La casilla está antes que la fila en el HTML, y eso no es casual: CSS puede bajar y puede ir hacia la derecha, pero no puede subir ni retroceder. El disparador tiene que venir antes que lo disparado.
La regla es una sola: solo se transiciona lo que tiene valores
intermedios. Entre el rojo y el azul hay mil colores; entre 0 y 20 px hay mil
medidas. Pero entre block y none no hay nada en medio.
| Sí transicionan | No transicionan |
|---|---|
colores (color, background-color, border-color) |
display |
medidas (width, height, padding, margin, font-size) |
position |
opacity |
flex-direction |
transform en todas sus formas |
font-family |
box-shadow, border-radius |
cualquier palabra sin punto medio |
El caso que siempre aparece: hacer aparecer algo con
display: none → display: block no se puede suavizar. La solución es
no usar display, sino opacity para el desvanecido y
visibility para que el elemento no quede estorbando cuando está invisible.
aparecer y desaparecer con opacity — pasa el ratón por la fila
<div class="row"> <div class="caja">pasa el ratón</div> <div class="caja marcada escondida">aparezco</div> </div>
.row { display: flex; gap: 10px; align-items: flex-start; } .caja { width: 90px; height: 70px; background: #dcdcea; border: 2px solid #8686a8; display: flex; align-items: center; justify-content: center; font-family: Verdana, sans-serif; font-size: .8rem; } .marcada { background: #9a9ac8; } .escondida { opacity: 0; visibility: hidden; transition: opacity 500ms, visibility 500ms; } .row:hover .escondida { opacity: 1; visibility: visible; }
visibility tampoco tiene valores intermedios, pero puesta en la lista
espera: al ocultar, aguanta los 500 ms que tarda el desvanecido antes de
retirar el elemento, en lugar de suprimirlo de inmediato. Es el procedimiento habitual.
Nótese que el hueco de la caja oculta sigue ahí: opacity: 0 y
visibility: hidden no sacan al elemento del flujo. Si lo que se quiere es que no
ocupe sitio, no hay transición posible y hay que usar display: none.
:hover.ease-out para
lo que aparece.transform y
opacity. Transicionar width o margin obliga a
recalcular la maqueta en cada fotograma, y en una página grande se nota.