Cómo hacer que los elementos pegajosos y de sangrado completo jueguen bien juntos | Programar Plus

Tenía un requisito único el otro día: construir un diseño con elementos de sangrado completo mientras un elemento permanece pegado en la parte superior. Esto terminó siendo bastante complicado de lograr, así que lo estoy documentando aquí en caso de que alguien necesite recrear este mismo efecto. Parte del engaño también fue lidiar con el posicionamiento lógico en pantallas pequeñas.

Es difícil describir el efecto, así que grabé mi pantalla para mostrar lo que quiero decir. Preste especial atención a la sección principal de llamada a la acción, la que tiene el encabezado “Pruebe Domino hoy”.

La idea es mostrar la llamada a la acción principal en el lado derecho mientras los usuarios se desplazan más allá de otras secciones en ventanas gráficas más grandes. En las ventanas gráficas más pequeñas, el elemento de llamada a la acción debe mostrarse después de la sección principal del héroe con el encabezado “Comience su prueba”.

Aquí hay dos desafíos principales:

  • Cree elementos de sangrado completo que no interfieran con el elemento adhesivo
  • Evite duplicar el HTML

Antes de sumergirnos en un par de posibles soluciones (y sus limitaciones), primero configuremos la estructura semántica HTML.

El HTML

Al crear este tipo de diseños, uno podría tener la tentación de crear secciones de llamado a la acción duplicadas: una para la versión de escritorio y otra para la versión móvil y luego alternar la visibilidad de ellas cuando sea apropiado. Esto evita tener que encontrar el lugar perfecto en el HTML y tener que aplicar CSS que maneje ambas necesidades de diseño. Debo admitir que soy culpable de hacer eso de vez en cuando. Pero esta vez, quería evitar duplicar mi HTML.

La otra cosa a considerar es que estamos usando el posicionamiento fijo en el .box--sticky elemento, lo que significa que debe ser hermano de otros elementos, incluidos los de sangrado completo, para que funcione correctamente.

Aquí está el marcado:

<div class="grid">

  <div class="box box--hero">Hero Box</div>

  <div class="box box--sticky">Sticky Box</div>

  <div class="box box--bleed">Full-bleed Box</div>
  <div class="box box--bleed">Full-bleed Box</div>
  <!-- a bunch more of these -->

</div>

Pongámonos pegajosos

Crear elementos adhesivos en un diseño de cuadrícula CSS es bastante sencillo. Añadimos position: sticky al .box--sticky elemento con un top: 0 offset, que indica dónde comienza a pegarse. Ah, y observe que solo estamos haciendo que el elemento sea pegajoso en las ventanas de visualización de más de 768 px.

@media screen and (min-width: 768px) {
  .box--sticky {
    position: sticky;
    top: 0;
  }
}

Tenga en cuenta que existe un problema conocido con el posicionamiento fijo en Safari cuando se usa con overflow: auto. Está documentado en caniuse en la sección de problemas conocidos:

Un padre con desbordamiento establecido en auto va a prevenir position: sticky de trabajar en Safari.

Bien, eso fue fácil. A continuación, resolvamos el desafío de los elementos puros.

Solución 1: Pseudo-elementos

La primera solución es algo que uso a menudo: pseudoelementos absolutamente posicionados que se extienden de un lado a otro. El truco aquí es utilizar un desplazamiento negativo.

Si hablamos de contenido centrado, entonces el cálculo es bastante sencillo:

.box--bleed {
  max-width: 600px;
  margin-right: auto;
  margin-left: auto;
  padding: 20px;
  position: relative; 
}

.box--bleed::before {
  content: "";
  background-color: dodgerblue; 
  position: absolute;
  top: 0;
  bottom: 0;
  right: calc((100vw - 100%) / -2);
  left: calc((100vw - 100%) / -2);
}

En resumen, el desplazamiento negativo es el ancho de la ventana gráfica, 100 vw, menos el ancho del elemento, 100%, y luego se divide por -2, porque necesitamos dos desplazamientos negativos.

Tenga en cuenta que hay un error conocido al usar 100vw, que también está documentado en caniuse:

Actualmente, todos los navegadores, excepto Firefox, consideran incorrectamente que 100vw es el ancho completo de la página, incluida la barra de desplazamiento vertical, que puede provocar una barra de desplazamiento horizontal cuando se establece overflow: auto.

Ahora hagamos elementos con sangrado completo cuando el contenido no esté centrado. Si vuelve a ver el video, observe que no hay contenido debajo del elemento adhesivo. No queremos que nuestro elemento fijo se superponga al contenido y esa es la razón por la que no tenemos contenido centrado en este diseño en particular.

Primero, vamos a crear la cuadrícula:

.grid {
  display: grid;
  grid-gap: var(--gap);
  grid-template-columns: var(--cols);
  max-width: var(--max-width);
  margin-left: auto;
  margin-right: auto;
}

Estamos usando propiedades personalizadas que nos permiten redefinir el ancho máximo, el espacio y las columnas de la cuadrícula sin redefinir las propiedades. En otras palabras, en lugar de redeclarar la grid-gap, grid-template-columns, y max-width properties, estamos volviendo a declarar los valores de las variables:

:root {
  --gap: 20px;
  --cols: 1fr;
  --max-width: calc(100% - 2 * var(--gap));
}

@media screen and (min-width: 768px) {
  :root {
    --max-width: 600px;
    --aside-width: 200px;
    --cols: 1fr var(--aside-width);
  }
}

@media screen and (min-width: 980px) {
  :root {
    --max-width: 900px;
    --aside-width: 300px;
  }
}

En las ventanas gráficas de 768 px de ancho o más, hemos definido dos columnas: una con un ancho fijo, --aside-width, y uno con eso llena el espacio restante, 1fr, así como el ancho máximo del contenedor de la cuadrícula, --max-width.

En las ventanas gráficas de menos de 768 px, hemos definido una sola columna y el espacio. El ancho máximo del contenedor de cuadrícula es el 100% de la ventana gráfica, menos los espacios en cada lado.

Ahora viene la parte divertida. El contenido no se centra en ventanas gráficas más grandes, por lo que el cálculo no es tan sencillo como podría pensar. Así es como se ve:

.box--bleed {
  position: relative;
  z-index: 0;
}

.box--bleed::before {
  content: "";
  display: block;
  position: absolute;
  top: 0;
  bottom: 0;
  left: calc((100vw - (100% + var(--gap) + var(--aside-width))) / -2);
  right: calc(((100vw - (100% - var(--gap) + var(--aside-width))) / -2) - (var(--aside-width)));
  z-index: -1;
}

En lugar de utilizar el 100% del ancho principal, tenemos en cuenta los anchos del espacio y el elemento adhesivo. Eso significa que el ancho del contenido en elementos con sangrado completo no excederá los límites del elemento héroe. De esa manera, nos aseguramos de que el elemento adhesivo no se superponga con ningún dato importante.

El desplazamiento a la izquierda es más simple porque solo necesitamos restar el ancho del elemento (100%), el espacio (--gap) y el elemento adhesivo (--aside-width) desde el ancho de la ventana gráfica (100vw).

left: (100vw - (100% + var(--gap) + var(--aside-width))) / -2);

El desplazamiento derecho es más complicado porque tenemos que agregar el ancho del elemento adhesivo al cálculo anterior, --aside-width, así como la brecha, --gap:

right: ((100vw - (100% + var(--gap) + var(--aside-width))) / -2) - (var(--aside-width) + var(--gap));

Ahora estamos seguros de que el elemento adhesivo no se superpone a ningún contenido en los elementos con sangrado completo.

Aquí está la solución con un error horizontal:

Y aquí está la solución con una corrección de errores horizontal:

La solución es ocultar el desbordamiento en el eje x del cuerpo, lo que podría ser una buena idea en general de todos modos:

body {
  max-width: 100%;
  overflow-x: hidden;
}

Esta es una solución perfectamente viable y podríamos terminar aquí. Pero, ¿dónde está la diversión en eso? Por lo general, hay más de una forma de lograr algo, así que veamos otro enfoque.

Solución 2: cálculos de relleno

En lugar de usar un contenedor de cuadrícula centrado y pseudo elementos, podríamos lograr el mismo efecto configurando nuestra cuadrícula. Comencemos por definir la cuadrícula tal como lo hicimos la última vez:

.grid {
  display: grid;
  grid-gap: var(--gap);
  grid-template-columns: var(--cols);
}

Nuevamente, estamos usando propiedades personalizadas para definir el espacio y las columnas de la plantilla:

:root {
  --gap: 20px;
  --gutter: 1px;
  --cols: var(--gutter) 1fr var(--gutter);
}

Estamos mostrando tres columnas en ventanas gráficas de menos de 768 px. La columna central ocupa tanto espacio como sea posible, mientras que las otras dos se utilizan solo para forzar el espacio horizontal.

@media screen and (max-width: 767px) {
  .box {
    grid-column: 2 / -2;
  }
}

Tenga en cuenta que todos los elementos de la cuadrícula se colocan en la columna central.

En las ventanas gráficas de más de 768 px, estamos definiendo un --max-width variable que limita el ancho de las columnas interiores. También estamos definiendo --aside-width, el ancho de nuestro elemento adhesivo. Nuevamente, de esta manera nos aseguramos de que el elemento adhesivo no se coloque sobre ningún contenido dentro de los elementos de sangrado completo.

:root {
  --gap: 20px;
}

@media screen and (min-width: 768px) {
  :root {
    --max-width: 600px;
    --aside-width: 200px;
    --gutter: calc((100% - (var(--max-width))) / 2 - var(--gap));
    --cols: var(--gutter) 1fr var(--aside-width) var(--gutter);
  }
}

@media screen and (min-width: 980px) {
  :root {
    --max-width: 900px;
    --aside-width: 300px;
  }
}

A continuación, calculamos el ancho de la canaleta. El calculo es:

--gutter: calc((100% - (var(--max-width))) / 2 - var(--gap));

… Donde 100% es el ancho de la ventana gráfica. Primero, restamos el ancho máximo de las columnas internas del ancho de la ventana gráfica. Luego, dividimos ese resultado entre 2 para crear las canaletas. Finalmente, restamos el espacio de la cuadrícula para obtener el ancho correcto de las columnas de la canaleta.

Ahora empujemos el .box--hero elemento encima para que comience en la primera columna interior de la cuadrícula:

@media screen and (min-width: 768px) {
  .box--hero {
    grid-column-start: 2;
  }
}

Esto empuja automáticamente la caja adhesiva para que comience justo después del elemento héroe. También podríamos definir explícitamente la ubicación de la caja adhesiva, así:

.box--sticky {
  grid-column: 3 / span 1;
}

Finalmente, hagamos los elementos de sangrado completo configurando grid-column a 1 / -1. Eso le dice a los elementos que comiencen el contenido en el primer elemento de la cuadrícula y se extiendan hasta el último.

@media screen and (min-width: 768px) {  
  .box--bleed {
    grid-column: 1 / -1;
  }
}

Para centrar el contenido, vamos a calcular el padding izquierdo y derecho. El relleno de la izquierda es igual al tamaño de la columna de la canaleta, más el espacio de la rejilla. El relleno derecho es igual al tamaño del relleno izquierdo, más otro espacio en la cuadrícula, así como el ancho del elemento adhesivo.

@media screen and (min-width: 768px) {
  .box--bleed {  
    padding-left: calc(var(--gutter) + var(--gap));
    padding-right: calc(var(--gutter) + var(--gap) + var(--gap) + var(--aside-width));
  }
}

Aquí está la solución final:

Prefiero esta solución a la primera porque no utiliza unidades de visualización con errores.

Me encantan los cálculos CSS. El uso de operaciones matemáticas no siempre es sencillo, especialmente cuando se combinan diferentes unidades, como el 100%. Averiguar qué significa el 100% es la mitad del esfuerzo.

También me encanta resolver diseños simples pero complicados, como este, usando solo CSS. El CSS moderno tiene soluciones nativas, como la cuadrícula, el posicionamiento fijo y los cálculos, que eliminan las soluciones JavaScript complicadas y algo pesadas. ¡Dejemos el trabajo sucio para el navegador!

¿Tiene una solución mejor o un enfoque diferente para esto? Me encantaría saberlo.