wandres.dev
CONTEXTOS DE FORMATO · BFC, IFC y el flujo normal

Tres árboles que no son el mismo

Contexto de formato, bloque contenedor y contexto de apilamiento se calculan sobre el mismo DOM con reglas distintas: separarlos es el paso que convierte el CSS en algo predecible.

⏱ 21 min

Sobre un único árbol de DOM, el motor construye al menos tres estructuras independientes: el árbol de contextos de formato, que decide cómo se colocan las cajas; la cadena de bloques contenedores, que decide respecto a qué se resuelven los porcentajes y las posiciones; y el árbol de contextos de apilamiento, que decide quién se pinta encima de quién. La mayoría de la frustración con CSS viene de creer que son la misma cosa y de aplicar la intuición de una a las otras dos.

🎯 Al terminar esta lección sabrás
  • Distinguir las tres estructuras y qué pregunta responde cada una.
  • Identificar el bloque contenedor de una caja según su position.
  • Explicar por qué un mismo ancestro puede pertenecer a una estructura y no a otra.
  • Aplicar un procedimiento de diagnóstico que elija primero el árbol correcto.

Tres preguntas, tres estructuras

Un contexto de formato responde a cómo se colocan estas cajas entre sí: dirección de apilado, colapso de márgenes, interacción con flotantes. Lo abre la lista cerrada del catálogo que ya conoces.

Un bloque contenedor responde a respecto a qué se mide esta caja: contra qué se resuelve un width: 50%, desde dónde cuenta un inset-block-start: 0. Para una caja en flujo normal es la caja de contenido del ancestro de bloque más cercano. Para una caja absolute, es el área de relleno del ancestro posicionado más cercano. Para una fixed, es el viewport, salvo excepción.

Un contexto de apilamiento responde a quién se pinta encima de quién: el orden de pintado y el ámbito en el que un z-index tiene sentido. Lo abre otra lista distinta: opacity menor que 1, transform, filter, will-change, isolation: isolate, un elemento posicionado con z-index numérico, un elemento flex o grid con z-index numérico.

Las tres listas se solapan sin coincidir, y ahí está la trampa. position: absolute abre las tres cosas. overflow: hidden abre un contexto de formato pero no un bloque contenedor ni un contexto de apilamiento. opacity: 0.99 abre un contexto de apilamiento y no abre ninguno de los otros dos. transform abre un contexto de apilamiento y además convierte al elemento en bloque contenedor de sus descendientes fixed, que es la causa del bug de la cabecera fija que deja de estar fija.

propiedad contexto de formato bloque contenedor contexto de apilamiento
overflow: hidden no no
display: flow-root no no
position: relative no sí, para abspos solo con z-index
position: absolute sí, para abspos solo con z-index
transform: translateZ(0) no sí, incluso para fixed
opacity: 0.9 no no
display: flex no solo con z-index en hijos
contain: layout sí, para abspos y fixed
⚠️
El bug de la cabecera fija

Una cabecera con position: fixed dentro de un ancestro con cualquier transform, filter o will-change: transform deja de estar fija respecto al viewport y pasa a estarlo respecto a ese ancestro. No es un bug de tu CSS: es que transform promueve al ancestro a bloque contenedor de los descendientes fijos. Ocurre muchísimo con librerías de animación que aplican transform a un contenedor de página.

El mismo ancestro en tres papeles distintos

Este ejemplo produce tres respuestas diferentes para la misma pregunta “quién manda aquí”:

<div class="marco">
  <div class="panel">
    <div class="pieza">contenido</div>
  </div>
</div>
.marco { position: relative; }
.panel { overflow: auto; }
.pieza { position: absolute; inset-block-start: 0; inline-size: 50%; }

La caja .pieza está en el contexto de formato que abrió .panel, porque overflow: auto lo establece. Pero su bloque contenedor es .marco, porque es el ancestro posicionado más cercano y overflow no posiciona nada: ese 50% se mide contra .marco, no contra .panel, y ese inset-block-start: 0 la coloca en el techo de .marco aunque .panel esté desplazado por el scroll. Y no hay ningún contexto de apilamiento por medio, así que su z-index compite directamente con el de cualquier elemento del contexto raíz.

Tres ancestros distintos para tres preguntas distintas, y ni uno solo de los tres es “el padre”. Intentar razonar sobre esto con la intuición del árbol de DOM no funciona.

Los contextos de formato modernos y sus hijos

Flex y grid añaden un matiz que conviene tener presente. El contenedor establece un contexto de formato independiente para sus elementos, pero además cada elemento establece uno propio para su contenido. Eso significa que la cadena de colapsos de márgenes se rompe dos veces y que los flotantes internos de un elemento flex quedan encerrados en él sin que tengas que hacer nada.

También cambia el display de los hijos. Un elemento flex o grid sufre blockification: su display computado se convierte en el equivalente de bloque. Un span con display: inline dentro de un contenedor flex se comporta como block; un inline-flex se convierte en flex. Por eso las alturas y los márgenes verticales, que en un span no hacían nada, empiezan a funcionar en cuanto su padre es flex.

/* El span de dentro acepta block-size porque ha sido blockificado */
.barra { display: flex; }
.barra span { block-size: 3rem; }   /* funciona */

/* Fuera de flex, esto no hace nada */
.texto span { block-size: 3rem; }   /* ignorado, sigue siendo inline */

Hay una excepción que conviene conocer: display: contents en un hijo hace que su caja desaparezca y que sus hijos pasen a ser elementos del contenedor. No es blockification, es eliminación de la caja, y arrastra consecuencias que van más allá del layout.

Un procedimiento de diagnóstico

Cuando algo no cuadra, la primera decisión no es qué propiedad tocar, sino qué árbol mirar. El síntoma te lo dice.

Si el síntoma es de posición o de tamaño —un porcentaje que da un número inesperado, un absolute que aparece donde no debe, un fixed que se mueve con el scroll— el árbol es el de bloques contenedores. Sube buscando el primer ancestro posicionado, y si hay fixed de por medio, busca también transform, filter y contain.

Si el síntoma es de espaciado o de colocación relativa —márgenes que se funden, altura que ignora contenido, flotantes que sobresalen— el árbol es el de contextos de formato. Sube buscando el primer ancestro del catálogo del nivel 14.2.

Si el síntoma es de quién tapa a quién —un z-index que no obedece, un elemento que insiste en quedar debajo— el árbol es el de apilamiento. Sube buscando opacity, transform, filter, will-change e isolation.

Las DevTools ayudan en los tres casos, pero de formas distintas: el panel de caja calculada delata el colapso de márgenes, el resaltado del elemento al pasar el ratón delata el bloque contenedor equivocado, y el panel de capas delata los contextos de apilamiento inesperados.

CSS no tiene un modelo de objetos, tiene varios grafos superpuestos

La razón por la que un ingeniero competente puede pasar años sintiéndose torpe con CSS es que llega esperando un modelo de objetos: un árbol, unos nodos, unas propiedades que se resuelven hacia arriba. Y lo que hay es otra cosa: un DOM sobre el que se proyectan varios grafos independientes, cada uno con su propia regla de aristas. El árbol de contextos de formato, la cadena de bloques contenedores y el árbol de apilamiento no son vistas del mismo grafo; son grafos distintos que comparten los nodos. Un ancestro puede ser tu padre en uno, un extraño en otro y tu techo absoluto en el tercero. Ninguna intuición transferida de un lenguaje de árboles funciona aquí, porque en un lenguaje de árboles la relación padre-hijo es una y solo una. Y una vez lo aceptas, aparece la recompensa: cada uno de esos grafos es, por separado, completamente determinista y bastante simple. Las reglas de cada uno caben en media página. Lo que no cabe en ninguna página es la intersección de los tres razonada a la vez, y por eso la técnica no consiste en saber más CSS sino en tener la disciplina de elegir un grafo antes de empezar a pensar. Los ingenieros que parecen tener superpoderes con el layout no saben más propiedades que tú: saben cuál de los tres tableros están mirando.

⚔️ Separa los tres grafos
  1. Monta el ejemplo de .marco, .panel y .pieza y verifica en DevTools contra qué se resuelve el 50%.
  2. Añade position: relative a .panel y observa cómo cambia el bloque contenedor sin cambiar el contexto de formato.
  3. Coloca una cabecera fixed dentro de un contenedor con transform: translateZ(0) y comprueba que deja de ser fija. Quita el transform y confírmalo.
  4. Aplica opacity: 0.999 a un contenedor con hijos que usen z-index y explica el cambio de orden de pintado.
  5. Para cada uno de los cuatro experimentos, di qué grafo has modificado y cuáles has dejado intactos.