Dentro del polyfill de la consulta del contenedor

Gerald Monaco
Gerald Monaco

Las consultas de contenedor son una nueva función de CSS que te permite escribir una lógica de diseño que se oriente a atributos de un elemento superior (por ejemplo, su ancho o altura) para aplicarles diseño a sus elementos secundarios. Recientemente, se lanzó una gran actualización del polyfill, que coincidió con la llegada de la compatibilidad a los navegadores.

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

Detrás de escena

Transpilación

Cuando el analizador de CSS dentro de un navegador encuentra una regla at desconocida, como la nueva regla @container, la descarta como si nunca hubiera existido. Por lo tanto, lo primero y más importante que debe hacer el polyfill es transpilar una consulta @container en 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 cuando ves la fuente de CSS.

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

Antes de las consultas de contenedor, CSS no tenía una forma para que un autor habilitara o inhabilitara grupos de reglas de forma arbitraria. Para polyfill este comportamiento, también se deben transformar las reglas dentro de una consulta de contenedor. Cada @container tiene 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 establecerá 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 pseudoclase :where(...). Por lo general, incluir un selector de atributos adicional aumentaría la especificidad del selector. Con la pseudoclase, se puede aplicar la condición adicional y, al mismo tiempo, preservar la especificidad original. Para ver por qué esto es fundamental, 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 color: red, ya que la regla posterior siempre anularía la regla anterior con el mismo selector y especificidad. Por lo tanto, transpilar la primera regla y agregar un selector de atributos adicional sin :where(...) aumentaría la especificidad y causaría que color: blue se aplicara de forma errónea.

Sin embargo, la pseudoclase :where(...) es bastante nueva. Para los navegadores que no lo admiten, el polyfill proporciona una solución alternativa segura y sencilla: puedes aumentar de forma intencional la especificidad de tus reglas agregando manualmente un selector :not(.container-query-polyfill) ficticio 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:

  • El selector en el CSS de origen cambió, por lo que la diferencia en la especificidad es visible de forma explícita. Esto también actúa como documentación para que sepas qué se verá afectado cuando ya no necesites 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 simulador por el selector de atributos con la misma especificidad. Para evitar sorpresas, el polyfill usa ambos selectores: el selector de origen original se usa para determinar si el elemento debe recibir el atributo de polyfill, y el selector transpilado se usa para aplicar estilos.

Pseudoelementos

Una pregunta que podrías hacerte es la siguiente: si el polyfill establece algún atributo cq-XYZ en un elemento para incluir el ID de contenedor único 123, ¿cómo se pueden admitir los pseudoelementos, que no pueden tener atributos establecidos?

Los pseudoelementos siempre están vinculados a un elemento real en el DOM, llamado 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"]) (lo que no sería válido), el selector condicional se mueve al final del elemento de origen, #foo.

Sin embargo, eso no es todo lo que se necesita. Un contenedor no puede modificar nada que no esté contenido 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 del contenedor que se consulta. El atributo #foo[cq-XYZ] se cambiaría de forma errónea, y se aplicarían de forma errónea las reglas #foo.

Para corregir esto, el polyfill en realidad usa dos atributos: uno que solo puede aplicar un elemento superior a un elemento y uno que un elemento puede aplicar a sí mismo. El último atributo se usa 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 {
    /* ... */
  }
}

Dado que un contenedor nunca se aplicará el primer atributo (cq-XYZ-A), el primer selector solo coincidirá si un contenedor superior diferente cumplió con las condiciones del contenedor y lo aplicó.

Unidades relativas del contenedor

Las consultas de contenedor 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 más cercano y adecuado. Para admitirlos, la unidad se transforma en una expresión calc(...) con propiedades personalizadas de CSS. El polyfill establecerá los valores de estas propiedades a través de estilos intercalados en el elemento 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). Estos son un poco más complicados, ya que los ejes intercalados y de bloque se determinan según el writing-mode de el elemento que usa la unidad, no el elemento que se consulta. Para admitir esto, 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 CSS adecuada, al igual que antes.

Propiedades

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

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 funcione bien con otras funciones de CSS, como @supports. Esta funcionalidad es la base de las prácticas recomendadas para usar el 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, por ejemplo, que cualquier elemento secundario de .card tomará el valor de --cq-XYZ-container-name y --cq-XYZ-container-type. Esa no es la forma en que se comportan las propiedades nativas. Para resolver este problema, el polyfill insertará la siguiente regla antes de cualquier diseño del usuario, lo que garantizará que cada elemento reciba los valores iniciales, a menos que otra regla los anule de forma intencional.

* {
  --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 con consultas de contenedores más temprano que tarde, es importante brindarles a los visitantes restantes una buena experiencia.

Durante la carga inicial, hay mucho que debe suceder antes de que el polyfill pueda diseñar la página:

  • Se debe cargar y inicializar el polyfill.
  • Las hojas de estilo deben analizarse y transpilarse. Dado que no hay ninguna API para acceder a la fuente sin procesar de una hoja de estilo externa, es posible que debas volver a recuperarla de forma asíncrona, aunque lo ideal es hacerlo solo desde la caché del navegador.

Si el polyfill no aborda cuidadosamente estas inquietudes, es posible que se produzca una regresión en tus Métricas web esenciales.

Para que te resulte más fácil brindarles a tus visitantes una experiencia agradable, 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). Específicamente, el polyfill no garantiza que tus consultas de contenedor se evaluarán antes del primer procesamiento de imagen. Esto significa que, para brindar la mejor experiencia del usuario, debes asegurarte de que el contenido cuyo tamaño o posición se vea afectado por el uso de consultas de contenedor esté oculto hasta que el polyfill haya cargado y transpilado tu CSS. Una forma de hacerlo es con una regla @supports:

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

Te recomendamos que combines esto con una animación de carga pura de CSS, posicionada de forma absoluta sobre tu contenido (oculto), para indicarle al visitante que está sucediendo algo. Puedes encontrar una demostración completa de este enfoque aquí.

Se recomienda este enfoque por varios motivos:

  • Un cargador de CSS puro minimiza la sobrecarga para los usuarios con navegadores más nuevos y, al mismo tiempo, proporciona comentarios ligeros a los usuarios con navegadores más antiguos y redes más lentas.
  • Si combinas el posicionamiento absoluto del cargador con visibility: hidden, evitas el cambio de diseño.
  • Después de que se cargue el polyfill, esta condición @supports dejará de pasar y se revelará tu contenido.
  • En los navegadores con compatibilidad integrada para consultas de contenedor, la condición nunca se aprobará, 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 alguno.

No podemos esperar a ver y experimentar las increíbles cosas que crearás con ella.