Una descripción general básica 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 ideas sobre cómo crear 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 el formato de video, aquí tienes una versión de YouTube de esta publicación:
Descripción general
Las pestañas son un componente común de los sistemas de diseño, pero pueden adoptar muchas formas. Primero, se crearon las pestañas para computadoras de escritorio en el elemento <frame> y, ahora, tenemos
componentes móviles fluidos que animan el contenido según las propiedades físicas.
Todos intentan hacer lo mismo: ahorrar espacio.
Hoy en día, los aspectos esenciales de la experiencia del usuario de las pestañas son un área de navegación de 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 seleccionado en la navegación.
Tácticas web
En general, este componente me pareció bastante sencillo de compilar, gracias a algunas funciones clave de la plataforma web:
scroll-snap-pointspara interacciones elegantes de deslizamiento y teclado con posiciones de detención de desplazamiento adecuadas- Vínculos directos a través de hashes de URL para el anclaje de desplazamiento en la página controlado por el navegador y la compatibilidad con el uso compartido
- Compatibilidad con lectores de pantalla con lenguaje de marcado de elementos
<a>yid="#hash" prefers-reduced-motionpara habilitar las transiciones de fundido cruzado y el desplazamiento instantáneo en la página- La función web
@scroll-timelineen borrador para subrayar y cambiar el color de forma dinámica de la pestaña seleccionada
El HTML
Básicamente, la UX aquí es la siguiente: haz clic en un vínculo, haz que la URL represente el estado de la página anidada y, luego, observa cómo se actualiza el área de contenido a medida que el navegador se desplaza al elemento coincidente.
Hay algunos miembros de contenido estructurales, como vínculos y :target. Necesitamos
una lista de vínculos, para lo que un <nav> es ideal, y una lista de elementos
<article>, para lo que un <section> es ideal. Cada hash de vínculo coincidirá con una sección,
lo que permitirá que el navegador se desplace por medio de anclajes.
Por ejemplo, si haces clic en un vínculo, se 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 siempre. Es contenido complementario, como se indica en el lenguaje de marcado.
Usé el siguiente lenguaje de 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, completé los artículos con cantidades mixtas de lorem y los vínculos con un conjunto de títulos de imágenes y longitudes mixtas. Con el contenido para trabajar, podemos comenzar 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 de forma horizontal
- El área de contenido (azul) se puede desplazar de forma horizontal.
- Cada elemento de artículo (verde) se puede desplazar de forma vertical.
Hay 2 tipos diferentes de elementos involucrados en el desplazamiento:
- Una ventana
Un cuadro con dimensiones definidas que tiene el estilo de 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, por lo que el encabezado y la sección se ordenan verticalmente. Esta es nuestra primera ventana de desplazamiento, y oculta todo con overflow hidden. El encabezado y la sección de contenido pronto usarán el sobredesplazamiento 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 diagrama colorido 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 destaqué a continuación con VisBug nos ayudan a ver las ventanas que crearon los contenedores de desplazamiento.
Diseño de 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; }
El .snap-indicator debe desplazarse horizontalmente con el grupo de vínculos, y este diseño de encabezado ayuda a establecer ese escenario. No hay elementos de posicionamiento absoluto aquí.
A continuación, los estilos de desplazamiento. Resulta que podemos compartir los estilos de desplazamiento
entre nuestras 2 áreas de desplazamiento horizontal (encabezado y sección), por lo 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 un desbordamiento en el eje X, una contención de desplazamiento para atrapar el sobredesplazamiento, barras de desplazamiento ocultas para dispositivos táctiles y, por último, un ajuste de desplazamiento para bloquear las áreas de presentación del contenido. El orden de las pestañas del teclado es accesible y las interacciones guían el enfoque de forma natural. Los contenedores de ajuste de desplazamiento también obtienen una interacción agradable de estilo carrusel desde su teclado.
Diseño del encabezado de pestañas <nav>
Los vínculos de navegación deben estar dispuestos 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. ¡Excelente trabajo 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 tiene su propio estilo y 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 pestañas sea divertida, ya que el indicador ajusta su ancho al nuevo destino. Según la cantidad de elementos que haya aquí, el navegador renderizará una barra de desplazamiento o no.
Diseño de pestañas <section>
Esta sección es un elemento flexible y debe ser el consumidor dominante de espacio. También
necesita crear columnas para colocar 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 tanto como sea posible y, luego, para su propio diseño, crea una serie de columnas que tienen 100% del ancho del elemento superior. Los porcentajes funcionan muy bien aquí porque escribimos restricciones sólidas 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 este impulso de expansión), 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, que es exactamente lo que queremos, para desbordar la ventana superior.
A veces me cuesta entender estos conceptos. Este elemento de sección se ajusta a una caja, pero también creó un conjunto de cajas. Espero que las imágenes y las explicaciones te estén ayudando.
Diseño de pestañas <article>
El usuario debería poder desplazarse por el contenido del artículo, y las barras de desplazamiento solo deberían aparecer si hay desbordamiento. Estos elementos del artículo están en una posición ordenada. Son, al mismo tiempo, un elemento superior y uno secundario de desplazamiento. El navegador controla algunas interacciones táctiles, del mouse y del teclado que son difíciles para nosotros.
<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 de su desplazador superior. Me gusta mucho cómo los elementos de vínculo de navegación y los elementos de artículo se ajustan al inicio intercalado de sus respectivos contenedores de desplazamiento. Se ve 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 en la que queremos proporcionar la UX de desplazamiento. Esto significa que no necesito ningún estilo de altura o ancho aquí, solo necesito definir cómo se desborda. Establecí overflow-y en auto 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, elegí en la configuración del sistema "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 para mí revisar el diseño y la organizació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 flexbox o cuadrícula para un diseño.
Las Herramientas para desarrolladores pueden ayudarnos a visualizar esto:
Los diseños de desplazamiento están completos: se ajustan, se pueden vincular de forma directa y se puede acceder a ellos con el teclado. Base sólida para mejoras de UX, estilo y satisfacción.
Función destacada
Los elementos secundarios con ajuste de 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ébalo 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 visible 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í tienes 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 para que (con suerte) descubra todo el contenido sin problemas. Agregaré movimiento con un propósito y de forma condicional. Ahora, los usuarios pueden especificar sus preferencias de movimiento en el 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. La alineación no solo es una alineación bonita, sino que también fija 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 ser considerado.
Comportamiento de desplazamiento
Existe la oportunidad de mejorar el comportamiento de movimiento de :target y element.scrollIntoView(). De forma predeterminada, es instantánea. El navegador solo establece la posición de desplazamiento. ¿Qué pasa si queremos hacer una 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í presentamos movimiento, y un movimiento que el usuario no controla (como el desplazamiento), solo aplicamos este estilo si el usuario no tiene preferencia en su sistema operativo en torno al movimiento reducido. De esta manera, solo presentamos el movimiento de desplazamiento para las personas que están de acuerdo con él.
Indicador de pestañas
El objetivo de esta animación es ayudar a asociar el indicador con el estado del contenido. Decidí usar estilos de fundido cruzado de color border-bottom para los usuarios
que prefieren un movimiento reducido y una animación de deslizamiento vinculada al desplazamiento y de desvanecimiento de color
para los usuarios que no tienen problemas con el movimiento.
En las Herramientas para desarrolladores de Chromium, puedo activar o desactivar la preferencia y demostrar los 2 estilos de transición diferentes. Me divertí mucho creando 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 un movimiento reducido, ya que no lo necesito más. 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, sino que el color del 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 fundido cruzado de movimiento reducido y, en esta sección, te mostraré cómo vinculé el indicador y un área de desplazamiento. A continuación, veremos algunas cosas experimentales divertidas. 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 de esto es false, lo que significa que el usuario prefiere el movimiento reducido, no ejecutaremos ninguno de los efectos de movimiento de vinculación de desplazamiento.
if (motionOK) {
// motion based animation code
}
En el momento de escribir este artículo, la compatibilidad del navegador con
@scroll-timeline es nula. Es una
especificación preliminar que solo tiene
implementaciones experimentales. Sin embargo, tiene un polyfill, que uso en esta demostración.
ScrollTimeline
Si bien CSS y JavaScript pueden crear líneas de tiempo de desplazamiento, opté por 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 un elemento siga la posición de desplazamiento de otro y, para ello, creo un
ScrollTimeline que define el controlador del vínculo de desplazamiento, el scrollSource.
Por lo general, una animación en la Web se ejecuta en un intervalo 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 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 entre los que se debe animar, también conocidos como fotogramas clave.
Fotogramas clave dinámicos
Hay una forma muy potente de animar con CSS declarativo puro con @scroll-timeline, pero la animación que elegí era demasiado dinámica. No hay forma de realizar la transición entre el ancho de auto y no hay forma 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 sobre los elementos secundarios y tomaremos los valores calculados en el entorno 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 devuelve una cadena que la use como un valor translateX. Esto crea 4 fotogramas clave de transformación para la animación. Lo mismo se hace para el ancho: se pregunta a cada uno cuál es su ancho dinámico
y, luego, se usa como un valor de fotograma clave.
Este es un ejemplo de resultado, según 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 de desplazamiento del desplazador 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, viendo cómo cambian el ancho y la posición del indicador de una sección a la siguiente, con un seguimiento perfecto del desplazamiento.
Tal vez no lo hayas notado, pero me enorgullece mucho la transición de color cuando se selecciona el elemento de navegación destacado.
El gris más claro no seleccionado parece aún más alejado cuando el elemento destacado tiene más contraste. Es común realizar la transición del color del texto, como cuando se coloca el cursor sobre él y cuando se selecciona, pero es de nivel superior realizar la transición de ese color al desplazarse, sincronizado con el indicador de subrayado.
Así es como 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 sigue la misma línea de tiempo de desplazamiento que el indicador de subrayado. Uso la misma línea de tiempo que antes: como su función es emitir un tick en el desplazamiento, podemos usar ese tick en cualquier tipo de animación que queramos. Como hice antes, creo 4 fotogramas clave en el bucle y devuelvo 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 y, de lo contrario, es un color de texto estándar. El bucle anidado lo hace relativamente sencillo, 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 uso eso 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 bien un término para dispositivos móviles, pero creo que el propósito de los vínculos directos se cumple aquí con las pestañas, ya que puedes compartir una URL directamente con el contenido de una pestaña. El
navegador se desplazará en la página hasta el ID que coincida con el hash de la URL. Descubrí que este controlador onload generaba 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 o usan un teclado; a veces, solo se desplazan libremente, como deberían poder hacerlo. Cuando el desplazador de secciones deja de desplazarse, dondequiera que se detenga, debe coincidir con la barra de navegación superior.
A continuación, se muestra cómo esperar a que finalice el desplazamiento:
js
tabsection.addEventListener('scroll', () => {
clearTimeout(tabsection.scrollEndTimer);
tabsection.scrollEndTimer = setTimeout(determineActiveTabSection, 100);
});
Cada vez que se desplacen las secciones, borra el tiempo de espera de la sección si existe y, luego, inicia uno nuevo. Cuando se detenga el desplazamiento por las secciones, no borres el tiempo de espera y activa el evento 100 ms después de que se detenga. Cuando se active, llama a la función que busca determinar 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 ajustó, 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 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 de desplazamiento horizontal,
anidamos una consulta de medios que aplica
el desplazamiento smooth si el usuario tolera el movimiento. JavaScript puede realizar llamadas libremente para desplazar elementos a la vista, y CSS puede administrar la UX de forma declarativa.
A veces, forman una combinación encantadora.
Conclusión
Ahora que sabes cómo lo hice, ¿cómo lo harías tú? Esto hace que la arquitectura de componentes sea divertida. ¿Quién creará la primera versión con ranuras en su framework favorito? 🙂
Diversifiquemos nuestros enfoques y aprendamos todas las formas de crear en la Web. Si creas una demostración, envíame un tweet con tu versión y la agregaré a la siguiente sección de Remixes de la comunidad.
Remixes de la comunidad
- @devnook, @rob_dodson y @DasSurma con componentes web: artículo.
- @jhvanderschee con botones: Codepen.