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 de un navegador encuentra una regla at desconocida, como la regla @container nueva, la descartará 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 se usan las APIs de CSSOM y se visualiza 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. A cada @container se le da 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(...). Normalmente, la inclusión de un selector de atributos adicional aumentará la especificidad del selector. Con la pseudoclase, se puede aplicar la condición adicional y, al mismo tiempo, preservar la especificidad original. Para entender por qué esto es tan importante, 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 e incluir un selector de atributos adicional sin :where(...) aumentaría la especificidad y harí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 del CSS de origen cambió, por lo que la diferencia de especificidad es visible de forma explícita. Esto también actúa como documentación para que sepas qué se ve 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 fuente original se usa para determinar si el elemento debe recibir el atributo de polyfill y el selector transpilado se usa para definir el estilo.

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 seudoelementos 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 admitirlas, la unidad se transforma en una expresión calc(...) con las 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 de 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 del 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 garantiza 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, deben suceder varias cosas para que el polyfill pueda diseñar la página:

  • Se debe cargar y inicializar el polyfill.
  • Las hojas de estilo se deben analizar y transpilar. Debido a que no existe ninguna API para acceder a la fuente sin procesar de una hoja de estilo externa, es posible que sea necesario volver a recuperarla de manera asincrónica, aunque idealmente solo desde la caché del navegador.

Si el polyfill no resuelve estas inquietudes, es posible que tus Métricas web esenciales vuelvan a generarse.

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). En concreto, el polyfill no garantiza que las consultas de tu contenedor se evaluarán antes de la primera pintura. 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 de CSS pura, 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í.

Este enfoque se recomienda por varios motivos:

  • Un cargador de CSS puro minimiza la sobrecarga para los usuarios con navegadores más nuevos, al 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, 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 algún inconveniente.

Estamos ansiosos por ver y experimentar las cosas increíbles que construirás con ella.