Una descripción general básica sobre cómo compilar un componente de pestañas similar a los que se encuentran en las apps para iOS y Android.
En esta publicación, quiero compartir mi pensamiento sobre la compilación de un componente de pestañas para la Web que sea responsivo, admita varias entradas de dispositivos y funcione en todos los navegadores. Prueba la demostración.
Si prefieres ver un video, aquí tienes una versión de esta publicación en YouTube:
Descripción general
Las pestañas son un componente común de los sistemas de diseño, pero pueden adoptar muchas formas. Primero, había pestañas para computadoras de escritorio compiladas en el elemento <frame>
y ahora tenemos componentes para dispositivos móviles que animan el contenido en función de las propiedades físicas.
Todos intentan hacer lo mismo: ahorrar espacio.
Hoy en día, lo esencial de la experiencia del usuario de las pestañas es un área de navegación con botones que activa o desactiva la visibilidad del contenido en un marco de visualización. Muchas áreas de contenido diferentes comparten el mismo espacio, pero se presentan de forma condicional según el botón que se seleccione en la navegación.
Tácticas web
En general, encontré que este componente es bastante sencillo de compilar, gracias a algunas funciones clave de la plataforma web:
scroll-snap-points
para elegantes interacciones de deslizamiento y teclado con posiciones adecuadas de paradas de desplazamiento- Vínculos directos a través de valores hash de URL para la anclaje de desplazamiento en la página y la compatibilidad con el uso compartido controlados por el navegador
- Compatibilidad con lectores de pantalla con lenguaje de marcado de elementos
<a>
yid="#hash"
prefers-reduced-motion
para habilitar las transiciones de encadenado y el desplazamiento instantáneo en la página- La función web
@scroll-timeline
en borrador para subrayar de forma dinámica y cambiar el color de la pestaña seleccionada
El código HTML
Básicamente, la UX aquí es: hacer clic en un vínculo, hacer que la URL represente el estado de la página anidada y, luego, ver la actualización del área de contenido a medida que el navegador se desplaza hasta el elemento coincidente.
Hay algunos miembros de contenido estructural: vínculos y :target
. Necesitamos una lista de vínculos, para la que es ideal un <nav>
, y una lista de elementos <article>
, para la que es ideal un <section>
. Cada hash de vínculo coincidirá con una sección, lo que permite que el navegador desplace los elementos mediante el anclado.
Por ejemplo, hacer clic en un vínculo enfoca automáticamente el artículo :target
en Chrome 89, sin necesidad de JS. Luego, el usuario puede desplazarse por el contenido del artículo con su dispositivo de entrada como de costumbre. Es contenido complementario, como se indica en el lenguaje de marcado.
Usé el siguiente marcado para organizar las pestañas:
<snap-tabs>
<header>
<nav>
<a></a>
<a></a>
<a></a>
<a></a>
</nav>
</header>
<section>
<article></article>
<article></article>
<article></article>
<article></article>
</section>
</snap-tabs>
Puedo establecer conexiones entre los elementos <a>
y <article>
con las propiedades href
y id
de la siguiente manera:
<snap-tabs>
<header>
<nav>
<a href="#responsive"></a>
<a href="#accessible"></a>
<a href="#overscroll"></a>
<a href="#more"></a>
</nav>
</header>
<section>
<article id="responsive"></article>
<article id="accessible"></article>
<article id="overscroll"></article>
<article id="more"></article>
</section>
</snap-tabs>
Luego, llené los artículos con cantidades variadas de lorem y los enlaces con una longitud combinada y un conjunto de imágenes de títulos. Con el contenido con el que trabajar, podemos comenzar con el diseño.
Diseños con desplazamiento
Hay 3 tipos diferentes de áreas de desplazamiento en este componente:
- La navegación (rosa) se puede desplazar horizontalmente
- El área de contenido (azul) se puede desplazar horizontalmente.
- Cada elemento del artículo (verde) se puede desplazar verticalmente.
Existen 2 tipos diferentes de elementos involucrados en el desplazamiento:
- Una ventana
Es un cuadro con dimensiones definidas que tiene el estilo de la propiedadoverflow
. - Una superficie de gran tamaño
En este diseño, son los contenedores de listas: vínculos de navegación, artículos de sección y contenido de artículos.
Diseño <snap-tabs>
El diseño de nivel superior que elegí fue flex (Flexbox). Establecí la dirección en column
, de modo que el encabezado y la sección estén ordenados verticalmente. Esta es nuestra primera ventana de desplazamiento y oculta todo con desbordamiento oculto. El encabezado y la sección pronto usarán el desplazamiento excesivo, como zonas individuales.
<snap-tabs> <header></header> <section></section> </snap-tabs>
snap-tabs { display: flex; flex-direction: column; /* establish primary containing box */ overflow: hidden; position: relative; & > section { /* be pushy about consuming all space */ block-size: 100%; } & > header { /* defend againstneeding 100% */ flex-shrink: 0; /* fixes cross browser quarks */ min-block-size: fit-content; } }
Volvamos al colorido diagrama de 3 desplazamientos:
<header>
ahora está preparado para ser el contenedor de desplazamiento (rosa).<section>
está preparado para ser el contenedor de desplazamiento (azul).
Los marcos que se destacan a continuación con VisBug nos ayudan a ver las ventanas que crearon los contenedores de desplazamiento.
Diseño de las pestañas <header>
El siguiente diseño es casi el mismo: uso flex para crear un orden vertical.
<snap-tabs> <header> <nav></nav> <span class="snap-indicator"></span> </header> <section></section> </snap-tabs>
header { display: flex; flex-direction: column; }
.snap-indicator
debe viajar de forma horizontal con el grupo de vínculos, y este diseño de encabezado ayuda a establecer esa etapa. No hay elementos con posicionamiento absoluto aquí.
A continuación, los estilos de desplazamiento. Resulta que podemos compartir los estilos de desplazamiento entre nuestras 2 áreas de desplazamiento horizontales (encabezado y sección), así que creé una clase de utilidad, .scroll-snap-x
.
.scroll-snap-x {
/* browser decide if x is ok to scroll and show bars on, y hidden */
overflow: auto hidden;
/* prevent scroll chaining on x scroll */
overscroll-behavior-x: contain;
/* scrolling should snap children on x */
scroll-snap-type: x mandatory;
@media (hover: none) {
scrollbar-width: none;
&::-webkit-scrollbar {
width: 0;
height: 0;
}
}
}
Cada uno necesita desbordamiento en el eje x, contención de desplazamiento para atrapar el desplazamiento excesivo, barras de desplazamiento ocultas para dispositivos táctiles y, por último, ajuste de desplazamiento para bloquear las áreas de presentación de contenido. Nuestro orden de tabulación del teclado es accesible y cualquier interacción guía el enfoque de forma natural. Los contenedores de ajuste de desplazamiento también obtienen una interacción con estilo de carrusel desde el teclado.
Diseño <nav>
del encabezado de pestañas
Los vínculos de navegación deben estar distribuidos en una línea, sin saltos de línea, centrados verticalmente, y cada elemento de vínculo debe ajustarse al contenedor de ajuste de desplazamiento. Swift funciona para CSS en 2021.
<nav> <a></a> <a></a> <a></a> <a></a> </nav>
nav { display: flex; & a { scroll-snap-align: start; display: inline-flex; align-items: center; white-space: nowrap; } }
Cada vínculo se aplica un diseño y un tamaño, por lo que el diseño de navegación solo necesita especificar la dirección y el flujo. Los anchos únicos en los elementos de navegación hacen que la transición entre las pestañas sea divertida, ya que el indicador ajusta su ancho al nuevo objetivo. Según la cantidad de elementos que haya aquí, el navegador renderizará una barra de desplazamiento o no.
Diseño <section>
de pestañas
Esta sección es un elemento flexible y debe ser el consumidor dominante de espacio. También debe crear columnas para que se coloquen los artículos. Una vez más, un trabajo rápido para CSS 2021. block-size: 100%
estira este elemento para llenar el elemento superior lo más posible y, luego, para su propio diseño, crea una serie de columnas que son 100%
el ancho del elemento superior. Los porcentajes funcionan muy bien aquí
porque escribimos restricciones estrictas en el elemento superior.
<section> <article></article> <article></article> <article></article> <article></article> </section>
section { block-size: 100%; display: grid; grid-auto-flow: column; grid-auto-columns: 100%; }
Es como si dijéramos “expande verticalmente tanto como sea posible, de forma insistente” (recuerda el encabezado que configuramos en flex-shrink: 0
: es una defensa contra esta expansión forzada), que establece la altura de la fila para un conjunto de columnas de altura completa. El estilo auto-flow
le indica a la cuadrícula que siempre coloque los elementos secundarios en una línea horizontal, sin ajuste, exactamente lo que queremos; desborde la ventana superior.
A veces, me resulta difícil entenderlos. Este elemento de la sección encaja en una caja, pero también creó un conjunto de cuadros. Espero que las imágenes y las explicaciones te ayuden.
Diseño <article>
de pestañas
El usuario debe poder desplazarse por el contenido del artículo, y las barras de desplazamiento solo deben aparecer si hay desbordamiento. Estos elementos del artículo están en una posición ordenada. Son, al mismo tiempo, un elemento superior y secundario del desplazamiento. El navegador realmente controla algunas interacciones táctiles, del mouse y del teclado difíciles por nosotros aquí.
<article> <h2></h2> <p></p> <p></p> <h2></h2> <p></p> <p></p> ... </article>
article { scroll-snap-align: start; overflow-y: auto; overscroll-behavior-y: contain; }
Elegí que los artículos se ajusten dentro del control deslizante superior. Me gusta mucho cómo los elementos de vínculo de navegación y los elementos del artículo se ajustan al inicio intercalado de sus respectivos contenedores de desplazamiento. Parece y se siente como una relación armoniosa.
El artículo es un elemento secundario de la cuadrícula, y su tamaño está predeterminado para ser el área del viewport que queremos proporcionar a la UX de desplazamiento. Esto significa que no necesito ningún estilo de altura ni ancho aquí, solo necesito definir cómo se desborda. Establecí overflow-y en automático y, luego, también capturé las interacciones de desplazamiento con la práctica propiedad overscroll-behavior.
Resumen de las 3 áreas de desplazamiento
A continuación, en la configuración del sistema, elegí "mostrar siempre las barras de desplazamiento". Creo que es doblemente importante que el diseño funcione con este parámetro de configuración activado, ya que es importante para mí revisar el diseño y la orquestación del desplazamiento.
Creo que ver el margen de la barra de desplazamiento en este componente ayuda a mostrar con claridad dónde están las áreas de desplazamiento, la dirección que admiten y cómo interactúan entre sí. Considera cómo cada uno de estos marcos de ventana de desplazamiento también son elementos superiores de cuadrícula o flex a un diseño.
DevTools puede ayudarnos a visualizar esto:
Los diseños de desplazamiento están completos: se ajustan, se pueden vincular con vínculos directos y son accesibles con el teclado. Una base sólida para las mejoras de UX, el estilo y la satisfacción.
Función destacada
Los elementos secundarios ajustados al desplazamiento mantienen su posición bloqueada durante el cambio de tamaño. Esto significa que JavaScript no necesitará mostrar nada durante la rotación del dispositivo ni el cambio de tamaño del navegador. Pruébala en el Modo de dispositivo de las herramientas para desarrolladores de Chromium. Para ello, selecciona cualquier modo que no sea Responsivo y, luego, cambia el tamaño del marco del dispositivo. Observa que el elemento permanece en la vista y bloqueado con su contenido. Esta función está disponible desde que Chromium actualizó su implementación para que coincida con la especificación. Aquí hay una entrada de blog al respecto.
Animación
El objetivo del trabajo de animación aquí es vincular claramente las interacciones con los comentarios de la IU. Esto ayuda a guiar o asistir al usuario en su descubrimiento (con suerte) y sin problemas de todo el contenido. Agregaré movimiento con propósito y condicionalmente. Ahora los usuarios pueden especificar sus preferencias de movimiento en su sistema operativo, y me encanta responder a sus preferencias en mis interfaces.
Vincularé un subrayado de pestaña con la posición de desplazamiento del artículo. El ajuste no solo es una alineación atractiva, sino que también ancla el inicio y el final de una animación.
Esto mantiene el <nav>
, que actúa como un minimapa, conectado al contenido.
Comprobaremos la preferencia de movimiento del usuario desde CSS y JS. Hay algunos lugares excelentes para hacerlo.
Comportamiento de desplazamiento
Hay una oportunidad para mejorar el comportamiento del movimiento de :target
y element.scrollIntoView()
. De forma predeterminada, es instantánea. El navegador solo establece la posición de desplazamiento. ¿Qué sucede si queremos hacer la transición a esa posición de desplazamiento, en lugar de parpadear allí?
@media (prefers-reduced-motion: no-preference) {
.scroll-snap-x {
scroll-behavior: smooth;
}
}
Como presentamos aquí el movimiento y el movimiento que el usuario no controla (como el desplazamiento), solo aplicamos este estilo si el usuario no tiene preferencia en su sistema operativo con respecto al movimiento reducido. De esta manera, solo presentamos el movimiento de desplazamiento para las personas que lo aceptan.
Indicador de pestañas
El propósito de esta animación es ayudar a asociar el indicador con el estado del contenido. Decidí aplicar un encadenado de colores con los estilos border-bottom
para los usuarios que prefieren un movimiento reducido y una animación de deslizamiento + atenuación de color vinculada con desplazamiento para los usuarios que no tienen problemas con el movimiento.
En Herramientas para desarrolladores de Chromium, puedo activar o desactivar la preferencia y demostrar los 2 estilos de transición diferentes. Me divertí mucho compilando esto.
@media (prefers-reduced-motion: reduce) {
snap-tabs > header a {
border-block-end: var(--indicator-size) solid hsl(var(--accent) / 0%);
transition: color .7s ease, border-color .5s ease;
&:is(:target,:active,[active]) {
color: var(--text-active-color);
border-block-end-color: hsl(var(--accent));
}
}
snap-tabs .snap-indicator {
visibility: hidden;
}
}
Oculto el .snap-indicator
cuando el usuario prefiere reducir el movimiento, ya que ya no lo necesito. Luego, lo reemplazo por estilos border-block-end
y una transition
. También observa en la interacción de las pestañas que el elemento de navegación activo no solo tiene un subrayado de marca destacado, sino que su color de texto también es más oscuro. El elemento activo tiene un contraste de color de texto más alto y un acento de luz inferior brillante.
Solo unas pocas líneas adicionales de CSS harán que alguien se sienta visto (en el sentido de que respetamos cuidadosamente sus preferencias de movimiento). Me encanta.
@scroll-timeline
En la sección anterior, te mostré cómo controlo los estilos de encadenado de movimiento reducido y, en esta sección, te mostraré cómo vinculé el indicador y un área de desplazamiento juntos. A continuación, te mostramos algunos elementos experimentales divertidos. Espero que estés tan emocionado como yo.
const { matches:motionOK } = window.matchMedia(
'(prefers-reduced-motion: no-preference)'
);
Primero verifico la preferencia de movimiento del usuario desde JavaScript. Si el resultado es false
, lo que significa que el usuario prefiere un movimiento reducido, no ejecutaremos ninguno de los efectos de movimiento de vinculación del desplazamiento.
if (motionOK) {
// motion based animation code
}
Al momento de escribir esto, no había compatibilidad con el navegador para @scroll-timeline
. Es un borrador de especificación con solo implementaciones experimentales. Sin embargo, tiene un polyfill, que uso en esta
demo.
ScrollTimeline
Si bien CSS y JavaScript pueden crear cronogramas de desplazamiento, habilité JavaScript para poder usar mediciones de elementos en vivo en la animación.
const sectionScrollTimeline = new ScrollTimeline({
scrollSource: tabsection, // snap-tabs > section
orientation: 'inline', // scroll in the direction letters flow
fill: 'both', // bi-directional linking
});
Quiero que 1 elemento siga la posición de desplazamiento de otro elemento y, cuando creo un ScrollTimeline
, defino el controlador del vínculo de desplazamiento, scrollSource
.
Por lo general, una animación en la Web se ejecuta en función de una marca de tiempo global, pero con un sectionScrollTimeline
personalizado en la memoria, puedo cambiar todo eso.
tabindicator.animate({
transform: ...,
width: ...,
}, {
duration: 1000,
fill: 'both',
timeline: sectionScrollTimeline,
}
);
Antes de entrar en los fotogramas clave de la animación, creo que es importante señalar que el seguidor del desplazamiento, tabindicator
, se animará en función de un cronograma personalizado, el desplazamiento de nuestra sección. Esto completa la vinculación, pero le falta el ingrediente final, los puntos con estado para animar entre ellos, también conocidos como fotogramas clave.
Fotogramas clave dinámicos
Hay una forma muy potente de CSS declarativo puro para animar con @scroll-timeline
, pero la animación que elegí hacer era demasiado dinámica. No hay manera de realizar transiciones entre el ancho de auto
ni de crear de forma dinámica una cantidad de fotogramas clave según la longitud de los elementos secundarios.
Sin embargo, JavaScript sabe cómo obtener esa información, por lo que iteraremos por los elementos secundarios y tomaremos los valores calculados en el tiempo de ejecución:
tabindicator.animate({
transform: [...tabnavitems].map(({offsetLeft}) =>
`translateX(${offsetLeft}px)`),
width: [...tabnavitems].map(({offsetWidth}) =>
`${offsetWidth}px`)
}, {
duration: 1000,
fill: 'both',
timeline: sectionScrollTimeline,
}
);
Para cada tabnavitem
, desestructura la posición offsetLeft
y muestra una cadena que la use como un valor translateX
. Esto crea 4 fotogramas clave de transformación para la animación. Lo mismo sucede con el ancho. A cada uno se le pregunta cuál es su ancho dinámico y, luego, se usa como valor de fotograma clave.
Este es un resultado de ejemplo, basado en mis fuentes y preferencias del navegador:
Fotogramas clave de TranslateX:
[...tabnavitems].map(({offsetLeft}) =>
`translateX(${offsetLeft}px)`)
// results in 4 array items, which represent 4 keyframe states
// ["translateX(0px)", "translateX(121px)", "translateX(238px)", "translateX(464px)"]
Fotogramas clave de ancho:
[...tabnavitems].map(({offsetWidth}) =>
`${offsetWidth}px`)
// results in 4 array items, which represent 4 keyframe states
// ["121px", "117px", "226px", "67px"]
Para resumir la estrategia, el indicador de pestaña ahora se animará en 4 fotogramas clave según la posición de ajuste del desplazamiento del control deslizante de la sección. Los puntos de ajuste crean una delimitación clara entre nuestros fotogramas clave y realmente contribuyen a la sensación de sincronización de la animación.
El usuario controla la animación con su interacción, y ve cómo el ancho y la posición del indicador cambian de una sección a la siguiente, haciendo un seguimiento perfecto con el desplazamiento.
Es posible que no lo hayas notado, pero me siento muy orgulloso de la transición de color a medida que se selecciona el elemento de navegación destacado.
El gris más claro no seleccionado parece aún más retrocedido cuando el elemento destacado tiene más contraste. Es común hacer la transición de color para el texto, como cuando se coloca el cursor sobre él y cuando se selecciona, pero es un nivel superior hacer la transición de ese color al desplazarse, sincronizado con el indicador de subrayado.
A continuación, te indico cómo lo hice:
tabnavitems.forEach(navitem => {
navitem.animate({
color: [...tabnavitems].map(item =>
item === navitem
? `var(--text-active-color)`
: `var(--text-color)`)
}, {
duration: 1000,
fill: 'both',
timeline: sectionScrollTimeline,
}
);
});
Cada vínculo de navegación de pestaña necesita esta nueva animación de color, que realiza un seguimiento del mismo cronograma de desplazamiento que el indicador de subrayado. Uso el mismo cronograma que antes: dado que su función es emitir una marca durante el desplazamiento, podemos usarla en cualquier tipo de animación que queramos. Al igual que antes, creo 4 fotogramas clave en el bucle y devuelvo los colores.
[...tabnavitems].map(item =>
item === navitem
? `var(--text-active-color)`
: `var(--text-color)`)
// results in 4 array items, which represent 4 keyframe states
// [
"var(--text-active-color)",
"var(--text-color)",
"var(--text-color)",
"var(--text-color)",
]
El fotograma clave con el color var(--text-active-color)
destaca el vínculo; de lo contrario, será de un color de texto estándar. El bucle anidado allí lo hace relativamente simple, ya que el bucle externo es cada elemento de navegación y el bucle interno son los fotogramas clave personales de cada elemento de navegación. Verifico si el elemento del bucle externo es el mismo que el del bucle interno y lo uso para saber cuándo se selecciona.
Me divertí mucho escribiendo esto. Mucho.
Aún más mejoras de JavaScript
Vale la pena recordar que el núcleo de lo que te muestro aquí funciona sin JavaScript. Dicho esto, veamos cómo podemos mejorarlo cuando JS esté disponible.
Vínculos directos
Los vínculos directos son más un término para dispositivos móviles, pero creo que el objetivo del vínculo directo se cumple aquí con las pestañas, ya que puedes compartir una URL directamente en el contenido de una pestaña. El navegador navegará en la página hasta el ID que coincida con el hash de la URL. Encontré que este controlador onload
generó el efecto en todas las plataformas.
window.onload = () => {
if (location.hash) {
tabsection.scrollLeft = document
.querySelector(location.hash)
.offsetLeft;
}
}
Sincronización del final del desplazamiento
Nuestros usuarios no siempre hacen clic ni usan un teclado. A veces, solo se desplazan libremente, como deberían poder hacerlo. Cuando el desplazador de secciones deje de desplazarse, su lugar debe coincidir con el que se muestra en la barra de navegación superior.
A continuación, te indico cómo espero el final del desplazamiento:
js
tabsection.addEventListener('scroll', () => {
clearTimeout(tabsection.scrollEndTimer);
tabsection.scrollEndTimer = setTimeout(determineActiveTabSection, 100);
});
Cada vez que se desplazan las secciones, borra el tiempo de espera de la sección si hay uno y comienza uno nuevo. Cuando se dejen de desplazar las secciones, no borres el tiempo de espera y evítalo 100 ms después del descanso. Cuando se active, llama a la función que busca averiguar dónde se detuvo el usuario.
const determineActiveTabSection = () => {
const i = tabsection.scrollLeft / tabsection.clientWidth;
const matchingNavItem = tabnavitems[i];
matchingNavItem && setActiveTab(matchingNavItem);
};
Si suponemos que el desplazamiento se ajusta, dividir la posición de desplazamiento actual por el ancho del área de desplazamiento debería dar como resultado un número entero y no un decimal. Luego, intento tomar un elemento de navegación de nuestra caché a través de este índice calculado y, si encuentra algo, envío la coincidencia para que se establezca como activa.
const setActiveTab = tabbtn => {
tabnav
.querySelector(':scope a[active]')
.removeAttribute('active');
tabbtn.setAttribute('active', '');
tabbtn.scrollIntoView();
};
Para configurar la pestaña activa, primero se borra cualquier pestaña activa y, luego, se le asigna el atributo de estado activo al elemento de navegación entrante. La llamada a scrollIntoView()
tiene una interacción divertida con CSS que vale la pena destacar.
.scroll-snap-x {
overflow: auto hidden;
overscroll-behavior-x: contain;
scroll-snap-type: x mandatory;
@media (prefers-reduced-motion: no-preference) {
scroll-behavior: smooth;
}
}
En la utilidad de ajuste de desplazamiento horizontal de CSS, anidamos una consulta de medios que aplica el desplazamiento smooth
si el usuario es tolerante al movimiento. JavaScript puede realizar llamadas de forma libre para desplazar elementos a la vista, y CSS puede administrar la UX de forma declarativa.
A veces, hacen una combinación muy agradable.
Conclusión
Ahora que sabes cómo lo hice, ¿cómo lo harías? Esto da como resultado una arquitectura de componentes divertida. ¿Quién hará la 1ª versión con ranuras en su marco de trabajo favorito? 🙂
Diversifiquemos nuestros enfoques y aprendamos todas las formas de desarrollar en la Web. Crea un error, twittea tu versión y la agregaré a la sección Remixes de la comunidad que se encuentra a continuación.
Remixes de la comunidad
- @devnook, @rob_dodson y @DasSurma con componentes web: artículo.
- @jhvanderschee con botones: Codepen.