Dentro del polyfill de la consulta del contenedor

Gerald Monaco
Gerald Monaco

Las consultas de contenedores son una nueva función de CSS que te permite escribir una lógica de diseño orientada a componentes de un elemento superior (por ejemplo, su ancho o alto) para aplicar estilo a sus elementos secundarios. Hace poco, se lanzó una gran actualización de polyfill, que coincidió con la versión de compatibilidad para navegadores.

En esta publicación, podrás saber cómo funciona el polyfill, los desafíos que supera y las prácticas recomendadas para brindar una gran experiencia del usuario a los visitantes.

Detrás de escena

Transpilación

Cuando el analizador de CSS dentro de un navegador encuentra una regla at desconocida, como la regla @container nueva, simplemente la descartará como si nunca hubiera existido. Por lo tanto, lo primero y más importante que debe hacer polyfill es transpilar una consulta @container a algo que no se descartará.

El primer paso en la transpilación es convertir la regla @container de nivel superior en una consulta @media. Esto garantiza principalmente que el contenido permanezca agrupado. Por ejemplo, cuando usas las APIs de CSSOM y ves la fuente de CSS.

Antes
@container (width > 300px) {
  /* content */
}
Después
@media all {
  /* content */
}

Antes de las consultas de contenedores, CSS no tenía una forma para que un autor habilitara o inhabilitara de manera arbitraria grupos de reglas. Para polyfill este comportamiento, las reglas dentro de una consulta de contenedor también deben transformarse. A cada @container se le asigna su propio ID único (por ejemplo, 123), que se usa para transformar cada selector de modo que solo se aplique cuando el elemento tenga un atributo cq-XYZ que incluya este ID. El polyfill establece este atributo durante el tiempo de ejecución.

Antes
@container (width > 300px) {
  .card {
    /* ... */
  }
}
Después
@media all {
  .card:where([cq-XYZ~="123"]) {
    /* ... */
  }
}

Observa el uso de la seudoclase :where(...). Por lo general, incluir un selector de atributos adicionales aumentaría la especificidad del selector. Con la seudoclase, se puede aplicar la condición adicional mientras se conserva la especificidad original. Para ver por qué esto es crucial, considera el siguiente ejemplo:

@container (width > 300px) {
  .card {
    color: blue;
  }
}

.card {
  color: red;
}

Dado este CSS, un elemento con la clase .card siempre debe tener el valor color: red, ya que la regla posterior siempre anulará a la anterior con el mismo selector y la misma especificidad. Por lo tanto, la transpilación de la primera regla e incluir un selector de atributos adicional sin :where(...) aumentaría la especificidad y provocaría que color: blue se aplicara de forma errónea.

Sin embargo, la seudoclase :where(...) es bastante nueva. Para los navegadores que no lo admiten, polyfill proporciona una solución alternativa segura y sencilla: puedes aumentar de manera intencional la especificidad de tus reglas agregando manualmente un selector :not(.container-query-polyfill) de prueba a tus reglas @container:

Antes
@container (width > 300px) {
  .card {
    color: blue;
  }
}

.card {
  color: red;
}
Después
@container (width > 300px) {
  .card:not(.container-query-polyfill) {
    color: blue;
  }
}

.card {
  color: red;
}

Esto tiene varios beneficios:

  • Cambió el selector del CSS de origen, por lo que la diferencia en la especificidad se puede ver explícitamente. Esto también actúa como documentación para que sepas qué se ve afectado cuando ya no necesitas admitir la solución alternativa o el polyfill.
  • La especificidad de las reglas siempre será la misma, ya que el polyfill no la cambia.

Durante la transpilación, el polyfill reemplazará este elemento ficticio con el selector de atributos con la misma especificidad. Para evitar sorpresas, el polyfill usa ambos selectores: el selector de fuente original se usa para determinar si el elemento debe recibir el atributo polyfill y el selector transpilado se usa para aplicar estilo.

Seudoelementos

Quizás te preguntes lo siguiente: si el polyfill establece algún atributo cq-XYZ en un elemento para incluir el ID de contenedor único 123, ¿cómo se admiten los pseudoelementos, que no pueden tener atributos establecidos?

Los seudoelementos siempre están vinculados a un elemento real en el DOM, denominado elemento de origen. Durante la transpilación, el selector condicional se aplica a este elemento real:

Antes
@container (width > 300px) {
  #foo::before {
    /* ... */
  }
}
Después
@media all {
  #foo:where([cq-XYZ~="123"])::before {
    /* ... */
  }
}

En lugar de transformarse en #foo::before:where([cq-XYZ~="123"]) (que no sería válido), el selector condicional se mueve al final del elemento de origen, #foo.

Sin embargo, esto no es todo lo que necesitamos. Un contenedor no puede modificar nada que no esté dentro de él (y un contenedor no puede estar dentro de sí mismo), pero ten en cuenta que eso es exactamente lo que sucedería si #foo fuera el elemento contenedor que se consulta. Se modificaría por error el atributo #foo[cq-XYZ], y se aplicarían de forma errónea las reglas #foo.

Para corregirlo, el polyfill usa dos atributos: uno que solo puede aplicarse a un elemento a través de un elemento superior y otro que un elemento puede aplicarse a sí mismo. El último atributo se utiliza para los selectores que se orientan a pseudoelementos.

Antes
@container (width > 300px) {
  #foo,
  #foo::before {
    /* ... */
  }
}
Después
@media all {
  #foo:where([cq-XYZ-A~="123"]),
  #foo:where([cq-XYZ-B~="123"])::before {
    /* ... */
  }
}

Como un contenedor nunca aplicará el primer atributo (cq-XYZ-A) a sí mismo, el primer selector solo coincidirá si un contenedor superior diferente cumple con las condiciones del contenedor y lo aplicó.

Unidades relativas del contenedor

Las consultas de contenedores también incluyen algunas unidades nuevas que puedes usar en tu CSS, como cqw y cqh para el 1% del ancho y la altura (respectivamente) del contenedor superior adecuado más cercano. Para admitirlos, la unidad se transforma en una expresión calc(...) mediante propiedades personalizadas de CSS. El polyfill establecerá los valores de estas propiedades a través de estilos intercalados en el elemento del contenedor.

Antes
.card {
  width: 10cqw;
  height: 10cqh;
}
Después
.card {
  width: calc(10 * --cq-XYZ-cqw);
  height: calc(10 * --cq-XYZ-cqh);
}

También hay unidades lógicas, como cqi y cqb para el tamaño intercalado y el tamaño del bloque (respectivamente). Esto es un poco más complicado, ya que los ejes intercalados y de bloque se determinan por el elemento writing-mode del elemento que usa la unidad y no por el elemento que se consulta. Para ello, el polyfill aplica un estilo intercalado a cualquier elemento cuyo writing-mode difiera de su elemento superior.

/* Element with a horizontal writing mode */
--cq-XYZ-cqi: var(--cq-XYZ-cqw);
--cq-XYZ-cqb: var(--cq-XYZ-cqh);

/* Element with a vertical writing mode */
--cq-XYZ-cqi: var(--cq-XYZ-cqh);
--cq-XYZ-cqb: var(--cq-XYZ-cqw);

Ahora, las unidades se pueden transformar en la propiedad personalizada del CSS adecuada, como antes.

Propiedades

Las consultas de contenedores también agregan algunas propiedades nuevas de CSS, como container-type y container-name. Dado que las APIs como getComputedStyle(...) no se pueden usar con propiedades desconocidas o no válidas, también se transforman en propiedades personalizadas de CSS después de su análisis. Si una propiedad no se puede analizar (por ejemplo, porque contiene un valor no válido o desconocido), simplemente se deja sola para que el navegador la maneje.

Antes
.card {
  container-name: card-container;
  container-type: inline-size;
}
Después
.card {
  --cq-XYZ-container-name: card-container;
  --cq-XYZ-container-type: inline-size;
}

Estas propiedades se transforman cada vez que se descubren, lo que permite que el polyfill se pueda usar correctamente con otras funciones de CSS, como @supports. Esta funcionalidad es la base de las prácticas recomendadas para usar polyfill, como se explica a continuación.

Antes
@supports (container-type: inline-size) {
  /* ... */
}
Después
@supports (--cq-XYZ-container-type: inline-size) {
  /* ... */
}

De forma predeterminada, las propiedades personalizadas de CSS se heredan, lo que significa que, por ejemplo, cualquier elemento secundario de .card tomará el valor de --cq-XYZ-container-name y --cq-XYZ-container-type. Definitivamente, no es así como se comportan las propiedades nativas. Para resolver esto, el polyfill insertará la siguiente regla antes de que cualquier usuario aplique el estilo, lo que garantiza que cada elemento reciba los valores iniciales, a menos que otra regla la anule intencionalmente.

* {
  --cq-XYZ-container-name: none;
  --cq-XYZ-container-type: normal;
}

Prácticas recomendadas

Si bien se espera que la mayoría de los visitantes ejecuten navegadores con compatibilidad integrada de consultas de contenedores lo antes posible, es importante brindarles una buena experiencia al resto de los visitantes.

Durante la carga inicial, deben ocurrir muchas cosas para que el polyfill pueda diseñar la página:

  • Se debe cargar e inicializar el polyfill.
  • Las hojas de estilo deben analizarse y transpilarse. Como no hay APIs para acceder a la fuente sin procesar de una hoja de estilo externa, es posible que se deba volver a recuperar de forma asíncrona, aunque idealmente solo desde la caché del navegador.

Si el polyfill no aborda cuidadosamente estos problemas, es posible que se produzcan regresión en las Métricas web esenciales.

Para que puedas ofrecerles a tus visitantes una experiencia agradable con mayor facilidad, el polyfill se diseñó para priorizar el Retraso de primera entrada (FID) y el Cambio de diseño acumulado (CLS), posiblemente a expensas del Procesamiento de imagen con contenido más grande (LCP). En concreto, el polyfill no garantiza que las consultas de tu contenedor se evalúen antes del primer procesamiento de imagen. Esto significa que para obtener la mejor experiencia del usuario, debes asegurarte de que se oculte todo el contenido cuyo tamaño o posición se vería afectado por el uso de consultas de contenedores hasta que el polyfill haya cargado y transpilado tu CSS. Una forma de lograrlo es con una regla @supports:

@supports not (container-type: inline-size) {
  #content {
    visibility: hidden;
  }
}

Se recomienda que combine esto con una animación de carga de CSS pura, absolutamente posicionada sobre su contenido (oculto), para indicarle al visitante que algo está sucediendo. Puedes encontrar una demostración completa de este enfoque aquí.

Este enfoque se recomienda por varios motivos:

  • Un cargador de CSS puro minimiza la sobrecarga para los usuarios con navegadores más nuevos, al mismo tiempo que proporciona comentarios ligeros a aquellos que usan navegadores más antiguos y redes más lentas.
  • Si combinas el posicionamiento absoluto del cargador con visibility: hidden, se evita el cambio de diseño.
  • Después de que se cargue el polyfill, dejará de pasar esta condición @supports y se revelará tu contenido.
  • En navegadores con compatibilidad integrada para consultas de contenedores, la condición nunca pasará, por lo que la página se mostrará en la primera pintura como se espera.

Conclusión

Si te interesa usar consultas de contenedores en navegadores más antiguos, prueba el polyfill. No dudes en informar un problema si tienes algún inconveniente.

Estamos ansiosos por ver y experimentar las cosas increíbles que crearás con él.

Agradecimientos

Imagen hero de Dan Cristian Pădureeligibility en Unsplash.