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 lógicas de diseño orientadas a los componentes de un elemento superior (por ejemplo, su ancho o alto) para definir el estilo de sus elementos secundarios. Recientemente, se lanzó una gran actualización del polyfill, que coincidió con la compatibilidad en 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 a fin de brindar una experiencia del usuario excelente 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 de @container a algo que no se descarte.

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 contenedores, el CSS no tenía forma de que un autor habilitara o inhabilitara de forma arbitraria grupos de reglas. Para aplicar polyfills en 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"]) {
    /* ... */
  }
}

Ten en cuenta el uso de la seudoclase :where(...). Normalmente, la inclusión de un selector de atributos adicional aumentará la especificidad del selector. Con la seudoclase, se puede aplicar la condición adicional mientras se conserva 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 anterior con el mismo selector y la misma 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 seudoclase :where(...) es bastante nueva. Para los navegadores que no lo admiten, el polyfill proporciona una solución alternativa segura y sencilla: puedes aumentar intencionalmente 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 necesitas admitir la solución alternativa o el polyfill.
  • La especificidad de las reglas siempre será la misma, ya que el polyfill no lo cambia.

Durante la transpilación, el polyfill reemplazará este elemento ficticio con el selector de atributos de 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

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

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 en su lugar:

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, eso no es todo lo que necesitas. 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 lo que sucedería exactamente si #foo fuera el mismo elemento del contenedor que se consulta. Se cambiaría el atributo #foo[cq-XYZ] por error y se aplicaría cualquier regla #foo.

Para corregir esto, el polyfill en realidad usa dos atributos: uno que solo puede aplicarse a un elemento con un elemento superior y otro que un elemento puede aplicarse a sí mismo. El último atributo se usa para los selectores orientados 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) a sí mismo, 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 vienen con 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 admitirlas, la unidad se transforma en una expresión calc(...) con las propiedades personalizadas de CSS. El polyfill establecerá los valores de estas propiedades mediante 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). Estos son un poco más complicados, porque los ejes de intercalado y de bloque están determinados por el writing-mode del elemento que usa la unidad, no por el elemento que se consulta. Para ello, el polyfill aplica un diseño 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 correspondiente, igual que antes.

Propiedades

Las consultas de contenedores también agregan algunas propiedades de 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 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 la controle el navegador.

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 comporte a la perfección con otras funciones de CSS, como @supports. Esta funcionalidad es la base de las prácticas recomendadas para usar polyfill, que se describen 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 cualquier elemento secundario de .card tomará el valor de --cq-XYZ-container-name y --cq-XYZ-container-type. Definitivamente, no se comportan así las propiedades nativas. Para resolver esto, el polyfill insertará la siguiente regla antes de los estilos de los usuarios y garantizará que cada elemento reciba los valores iniciales, a menos que otra regla lo anule intencionalmente.

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

Prácticas recomendadas

Si bien se espera que, pronto, la mayoría de los visitantes utilicen navegadores con compatibilidad integrada para consultas en contenedores, sigue siendo importante ofrecer una buena experiencia al resto de los visitantes.

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

  • Se debe cargar e 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, puede que sea necesario volver a recuperarla de manera asincrónica, aunque lo ideal sería hacerlo 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 puedas brindar a los 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 evaluarán antes de la primera pintura. Esto significa que, para brindar la mejor experiencia del usuario, debes asegurarte de que todo el contenido cuyo tamaño o posición se vea afectado por las consultas de contenedor esté oculto 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 combinar esto con una animación de carga de CSS pura, absolutamente posicionada sobre tu contenido (oculto), para indicarle al visitante que algo está sucediendo. Puede 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, la condición @supports dejará de pasar 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 construirás con ella.

Agradecimientos

Hero image de Dan Cristian Pădureprivilege en Unsplash.