Una descripción general fundamental de 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 mis ideas 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 interacciones elegantes de deslizamiento y teclado con posiciones de detención de desplazamiento adecuadas- 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 transiciones de fundido cruzado y desplazamiento instantáneo dentro de la página- La función web
@scroll-timeline
en el borrador para subrayar y cambiar de color de forma dinámica la pestaña seleccionada
El código HTML
En esencia, la UX aquí es la siguiente: hacer clic en un vínculo, hacer que la URL represente el estado de la página anidada y, luego, ver cómo se actualiza el á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 permitirá que el navegador desplace los elementos a través del anclaje.
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 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>
A continuación, completé los artículos con cantidades variadas de lorem y los vínculos con una longitud y un conjunto de imágenes de títulos variados. Con el contenido con el que trabajar, podemos comenzar con el diseño.
Diseños de desplazamiento
Hay 3 tipos diferentes de áreas de desplazamiento en este componente:
- La barra de navegación (en 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; } }
Volvemos al diagrama colorido de 3 desplazamientos:
<header>
ahora está listo para ser el contenedor de desplazamiento (en rosa).<section>
está preparado para ser el contenedor de desplazamiento (azul).
Los marcos que destaqué a continuación con VisBug nos ayudan a ver las ventanas que crearon los contenedores de desplazamiento.
Diseño <header>
de pestañas
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; }
El .snap-indicator
debe desplazarse horizontalmente 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 buena interacción con el estilo de carrusel desde el teclado.
Diseño del encabezado <nav>
de las 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 trabaja para el CSS de 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 debe colocar los elementos secundarios en una línea horizontal, sin unión, exactamente lo que queremos, para desbordar la ventana superior.
A veces, me resulta difícil entenderlos. Este elemento de sección se ajusta a un cuadro, pero también creó un conjunto de cuadros. Espero que las imágenes y las explicaciones te resulten útiles.
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 de 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 con la que queremos proporcionar 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í la opción "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. Base sólida para mejoras de UX, estilo y placer.
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 cuando se rote el dispositivo o se cambie el 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 hasta que (con suerte) logre descubrir todo el contenido sin problemas. Agregaré movimiento de forma intencional y con condiciones. 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.
Verificaremos 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 aquí estamos introduciendo movimiento, y el movimiento que el usuario no controla (como el desplazamiento), solo aplicamos este estilo si el usuario no tiene preferencias en su sistema operativo sobre el 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í usar transiciones de color para los estilos border-bottom
para los usuarios que prefieren un movimiento reducido y una animación de desplazamiento deslizante vinculado con atenuación de color 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 un 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 manejo los estilos de transición de atenuación de movimiento reducido, y en esta sección, te mostraré cómo vinculé el indicador y un área de desplazamiento. A continuación, te mostramos algunos elementos experimentales divertidos. Espero que tengas el mismo entusiasmo que yo.
const { matches:motionOK } = window.matchMedia(
'(prefers-reduced-motion: no-preference)'
);
Primero, reviso 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
}
En el momento de escribir este documento, la compatibilidad del navegador con @scroll-timeline
es nula. 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, y cuando creo un ScrollTimeline
, defino el controlador del vínculo de desplazamiento, el 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 objeto vinculado al desplazamiento, tabindicator
, se animará en función de una línea de tiempo personalizada, el desplazamiento de nuestra sección. Esto completa la vinculación, pero 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. Se hace lo mismo con el ancho. Se le pregunta a cada uno cuál es su ancho dinámico y, luego, se usa como valor de fotogramas clave.
Este es un ejemplo de resultado, 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, y realiza 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 aparece aún más alejado 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. Usamos el mismo cronograma que antes: como su función es emitir una marca de verificación al desplazarse, podemos usar esa marca en cualquier tipo de animación que deseemos. 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)",
]
La fotograma clave con el color var(--text-active-color)
destaca el vínculo y, de lo contrario, es 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 la parte principal 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 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 control deslizante de la sección deja de desplazarse, el lugar en el que se detiene debe coincidir con 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 se supone 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 obtener un elemento navitem 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 el CSS de la utilidad de ajuste del desplazamiento horizontal, 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 genera 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 compilar en la Web. Crea un Glitch, tuitea tu versión y la agregaré a la sección Remixes de la comunidad que aparece a continuación.
Remixes de la comunidad
- @devnook, @rob_dodson y @DasSurma con componentes web: artículo.
- @jhvanderschee con botones: Codepen.