Clase 31 · Componentes interactivos modernos
La versión actual de lo que vimos en la clase 29: scroll snap para carruseles, popover y dialog para ventanas y menús, transiciones de entrada y salida, y animaciones por scroll y view transitions. Cada bloque termina con su demo.
Scroll snap: carruseles sin ID
El contenedor «imanta» el scroll a puntos concretos.
En la clase 29 el carrusel necesitaba un id por imagen y enlaces para cambiar. Con scroll snap basta un contenedor con overflow y un par de propiedades: el usuario desliza (ratón, trackpad o dedo) y cada imagen se queda perfectamente colocada.
scroll-snap-type
En el contenedor: eje y comportamiento.
Necesita que el contenedor tenga overflow (auto o scroll) en ese eje.
.carrusel {
display: flex;
overflow-x: auto;
scroll-snap-type: x mandatory;
}
/* eje: x, y o both */
/* siempre se detiene en un punto */
scroll-snap-type: x mandatory;
/* solo se detiene si está cerca de un punto */
scroll-snap-type: x proximity;
/* dónde se alinea cada elemento */
scroll-snap-align: start;
scroll-snap-align: center;
scroll-snap-align: end;
scroll-snap-align
En cada elemento: dónde se detiene.
.slide {
scroll-snap-align: center;
}
Con center la imagen queda centrada; con start pegada al inicio. También existe scroll-snap-stop: always para que no se salte ninguna imagen al deslizar rápido.
scroll-padding y scroll-snap vertical
Margen de seguridad y secciones a pantalla completa.
scroll-padding deja un margen en el contenedor (útil con cabeceras fijas). En vertical, es la base de las páginas «por pantallas».
.pagina {
height: 100vh;
overflow-y: auto;
scroll-snap-type: y mandatory;
}
.pagina section {
height: 100vh;
scroll-snap-align: start;
}
Scroll snap en funcionamiento
Desliza el carrusel horizontalmente con el ratón, el trackpad o el dedo.
<div class="carrusel">
<img class="slide" src="../dinamicos/img1.svg" alt="">
<img class="slide" src="../dinamicos/img2.svg" alt="">
<img class="slide" src="../dinamicos/img3.svg" alt="">
<img class="slide" src="../dinamicos/img4.svg" alt="">
</div>
.carrusel {
display: flex;
gap: 16px;
padding: 16px;
overflow-x: auto;
scroll-snap-type: x mandatory;
scroll-padding: 16px;
}
.slide {
flex: 0 0 80%;
height: 220px;
object-fit: cover;
border-radius: 12px;
scroll-snap-align: center;
}
Ventanas y menús con HTML nativo
El navegador ya sabe abrir, cerrar y poner capas por encima: no hace falta el truco de :target.
En la clase 29 construimos el popup y el menú con :target. Funciona, pero cambia la URL y no es accesible. HTML tiene dos soluciones nativas:
popover: menús, avisos y cajas emergentes.<dialog>: ventanas modales con fondo oscurecido.
Los dos están soportados por los navegadores actuales. Comprueba la compatibilidad en caniuse.com antes de usarlos en producción.
El atributo popover
Un botón y una caja, enlazados por el id.
- La caja lleva el atributo
popovery unid. - El botón lleva
popovertarget="id". - Se cierra solo al pulsar Esc o al hacer clic fuera.
- Mientras está abierta, podemos darle estilo con
:popover-open.
<button popovertarget="menu">Abrir menú</button>
<div id="menu" popover>
<a href="#">Inicio</a>
<a href="#">Clases</a>
<a href="#">Contacto</a>
</div>
#menu {
inset: 70px auto auto 16px;
margin: 0;
padding: 12px;
border: 0;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
}
#menu a {
display: block;
padding: 6px 10px;
color: #364f6b;
text-decoration: none;
}
#menu a:hover {
background-color: #e6f5d6;
}
#menu:popover-open {
background-color: #fff;
}
Las cajas con popover están ocultas por defecto, se muestran sobre todo lo demás (no necesitan z-index) y vienen centradas en pantalla; por eso en el ejemplo cambiamos inset y margin.
La etiqueta
Una ventana modal con fondo oscurecido y cierre con Esc.
Para abrirla como modal usamos showModal(); es la única línea de JavaScript del apartado. Un <form method="dialog"> con un botón cierra la ventana sin código.
<button onclick="ventana.showModal()">Abrir ventana</button>
<dialog id="ventana">
<h3>Ventana modal</h3>
<p>Se cierra con la tecla Esc o con el botón.</p>
<form method="dialog">
<button>Cerrar</button>
</form>
</dialog>
El fondo oscurecido es el pseudoelemento ::backdrop y se estiliza como cualquier otro:
dialog {
border: 0;
border-radius: 12px;
padding: 24px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
}
dialog::backdrop {
background-color: rgba(0, 0, 0, 0.6);
}
Popover y dialog en funcionamiento
Abre el menú y la ventana; prueba a cerrarlos con Esc.
<button class="open-btn" popovertarget="menu">Abrir menú</button>
<div id="menu" popover>
<a href="#">Inicio</a>
<a href="#">Clases</a>
<a href="#">Contacto</a>
</div>
<button class="open-btn" onclick="ventana.showModal()">Abrir ventana</button>
<dialog id="ventana">
<h3>Ventana modal</h3>
<p>Se cierra con la tecla Esc o con el botón.</p>
<form method="dialog">
<button class="open-btn">Cerrar</button>
</form>
</dialog>
#menu {
inset: 70px auto auto 16px;
margin: 0;
padding: 12px;
border: 0;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
}
#menu a {
display: block;
padding: 6px 10px;
color: #364f6b;
text-decoration: none;
}
#menu a:hover {
background-color: #e6f5d6;
}
#menu:popover-open {
background-color: #fff;
}
dialog {
border: 0;
border-radius: 12px;
padding: 24px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
}
dialog::backdrop {
background-color: rgba(0, 0, 0, 0.6);
}
Animar lo que aparece y desaparece
Hasta ahora, display no se podía animar.
En la clase 29 vimos que display: none no admite transición y que había que recurrir a opacity y visibility. Dos novedades de CSS lo resuelven:
transition-behavior: allow-discrete: permite quedisplay(yoverlay) participen en una transición.@starting-style: indica el estado desde el que arranca un elemento que acaba de aparecer.
/* display no se puede animar: pasa de none a block de golpe */
.caja {
display: none;
transition: opacity 0.4s;
}
.caja.visible {
display: block;
opacity: 1;
}
allow-discrete
Hace que display se anime al final (al ocultar) o al principio (al mostrar).
Se añade a cada propiedad «discreta» en la lista de transition. Para los popover y dialog añadimos también overlay, así el elemento se queda en la capa superior mientras dura la animación de salida.
.caja {
transition: opacity 0.4s, display 0.4s allow-discrete;
}
@starting-style
El estado «antes de aparecer».
Cuando un elemento pasa de display: none a visible, no tiene un estado anterior desde el que transicionar. Con @starting-style se lo damos.
.caja:popover-open {
opacity: 1;
}
@starting-style {
.caja:popover-open {
opacity: 0;
}
}
Entrada y salida en funcionamiento
La caja entra y sale con un fundido y un desplazamiento.
<button class="open-btn" popovertarget="caja">Mostrar</button>
<div id="caja" popover>
<h3>Hola</h3>
<p>Aparezco y desaparezco con animación.</p>
</div>
#caja {
width: 260px;
padding: 20px;
border: 0;
border-radius: 12px;
background-color: #e6f5d6;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
/* estado de salida (cerrado) */
opacity: 0;
transform: translateY(30px);
transition: opacity 0.4s, transform 0.4s, display 0.4s allow-discrete, overlay 0.4s allow-discrete;
}
/* estado abierto */
#caja:popover-open {
opacity: 1;
transform: translateY(0);
}
/* estado de entrada: desde dónde empieza al abrirse */
@starting-style {
#caja:popover-open {
opacity: 0;
transform: translateY(30px);
}
}
Animaciones ligadas al scroll
La animación avanza con el scroll en lugar de con el tiempo.
En la clase 20 vimos animation-timeline: scroll(), que usa el scroll de toda la página. Con view() la animación depende de cuánto se ve el propio elemento en pantalla: es la base de los efectos «aparecer al hacer scroll».
Las animaciones por scroll y las view transitions son muy recientes. Comprueba su compatibilidad antes de usarlas y piensa un resultado aceptable para navegadores que no las soporten.
animation-timeline: view()
Una animación normal con @keyframes y un cambio de línea de tiempo.
@keyframes aparecer {
from {
opacity: 0;
transform: translateY(60px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.tarjeta {
animation: aparecer linear both;
animation-timeline: view();
animation-range: entry 0% cover 40%;
}
animation-timeline: view(): el progreso depende de la visibilidad del elemento.animation-range: entry 0% cover 40%: la animación empieza cuando el elemento empieza a entrar y termina cuando ya ha recorrido el 40% de su trayecto.
Animación por scroll en funcionamiento
Haz scroll dentro de la ventana de la demo.
<section class="pagina">
<div class="tarjeta">Tarjeta 1</div>
<div class="tarjeta">Tarjeta 2</div>
<div class="tarjeta">Tarjeta 3</div>
<div class="tarjeta">Tarjeta 4</div>
<div class="tarjeta">Tarjeta 5</div>
<div class="tarjeta">Tarjeta 6</div>
</section>
@keyframes aparecer {
from {
opacity: 0;
transform: translateY(60px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.tarjeta {
animation: aparecer linear both;
animation-timeline: view();
animation-range: entry 0% cover 40%;
}
.tarjeta {
height: 140px;
margin-bottom: 24px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.4rem;
font-weight: 700;
color: #fff;
background-color: #3a8dde;
border-radius: 12px;
}
View transitions entre páginas
Una transición animada al cambiar de una página a otra, sin JavaScript.
Se activa con una regla @view-transition en las dos páginas. Los elementos que tengan el mismo view-transition-name en ambas se «transforman» de uno al otro (posición, tamaño y color).
@view-transition {
navigation: auto;
}
.caja {
view-transition-name: caja;
}
View transition en funcionamiento
Pulsa el enlace: la caja se transforma entre las dos páginas.
<h1>Página 1</h1>
<div class="caja"></div>
<a class="open-btn" href="transicion-pagina-2.html">Ir a la página 2</a>
@view-transition {
navigation: auto;
}
.caja {
view-transition-name: caja;
}
.caja {
width: 100px;
height: 100px;
background-color: #3a8dde;
border-radius: 12px;
margin-bottom: 20px;
}
<h1>Página 2</h1>
<div class="caja"></div>
<a class="open-btn" href="transicion-pagina-1.html">Volver a la página 1</a>
@view-transition {
navigation: auto;
}
.caja {
view-transition-name: caja;
}
.caja {
width: 320px;
height: 160px;
background-color: #6ea832;
border-radius: 80px;
margin: 0 0 20px auto;
}