preserve-3d y backface-visibility: la escena que se aplana
Qué hace transform-style, qué propiedades destruyen un contexto tridimensional aunque lo declares, y cómo se construye una tarjeta que se voltea sin trucos.
El valor inicial de transform-style es flat, y esa palabra significa algo muy concreto: los descendientes de un elemento se proyectan sobre el plano de ese elemento antes de seguir subiendo. Un cubo cuyas caras son nietos y no hijos, sin preserve-3d en el intermedio, no es un cubo: son seis rectángulos pegados a una superficie. Esta lección trata de la propiedad que propaga el espacio tridimensional, de la larga lista de cosas que lo destruyen sin avisar, y de backface-visibility, que decide si ves la parte de atrás de una cara que te da la espalda.
- Explicar qué significa aplanar un subárbol y cuándo ocurre.
- Enumerar las propiedades que anulan
transform-style: preserve-3dde forma forzosa. - Construir una tarjeta de dos caras que se voltea, sin superponer opacidades.
- Decidir entre
backface-visibility: hiddeny ocultar la cara trasera por otros medios.
Aplanar frente a propagar
Cuando un elemento tiene descendientes transformados en 3D, el motor tiene que decidir qué hacer con esa profundidad al componer. Con transform-style: flat —el valor inicial— renderiza el subárbol en una superficie plana y luego coloca esa superficie según la transformación del elemento. Las coordenadas z de los descendientes se pierden en el proceso: dejan de existir como profundidad y se convierten en simple orden de pintado.
Con transform-style: preserve-3d, el elemento no crea esa superficie intermedia. Sus hijos participan en el mismo espacio tridimensional que él, con sus coordenadas z intactas, y el motor los ordena por profundidad real y no por orden de documento. Esto último es lo que hace posible un cubo: la cara de atrás queda detrás de la de delante porque su z lo dice, no porque tú hayas puesto un z-index.
.escena { perspective: 900px; }
.cuerpo { transform-style: preserve-3d; rotate: y 30deg; }
.cuerpo .cara { position: absolute; inset: 0; }
.cuerpo .frontal { transform: translateZ(100px); }
.cuerpo .trasera { transform: rotateY(180deg) translateZ(100px); }
La cadena tiene tres piezas y hace falta que estén las tres: alguien pone la cámara —perspective—, alguien propaga el espacio —preserve-3d—, y las caras se colocan con transformaciones que incluyen una componente z. Si falta cualquiera de las tres, el resultado se ve mal de una forma distinta, y saber cuál falta a partir del síntoma es lo que separa depurar de probar cosas.
Lo que destruye un contexto 3D sin decírtelo
Aquí está la parte que hace perder tardes. transform-style: preserve-3d se ignora forzosamente cuando el elemento tiene ciertas propiedades que obligan al motor a componer su subárbol como una imagen plana. La especificación las llama propiedades de agrupamiento, y son las que crean un grupo de composición que no puede tener profundidad interna.
overflowcon cualquier valor distinto devisible, incluidohidden,auto,scrollyclip.opacitymenor que 1.filtercon cualquier valor distinto denone.backdrop-filtercon cualquier valor distinto denone.clip-pathcon cualquier valor distinto denone.mask,mask-image,mask-bordercon valor distinto del inicial.mix-blend-modecon cualquier valor distinto denormal.isolation: isolate.contain: paint, y por tanto tambiéncontain: contentycontain: strict.
La lista se solapa casi por completo con la de las propiedades que crean un contexto de apilamiento, y no es casualidad: son las que fuerzan al motor a materializar el subárbol en una superficie propia antes de componerlo. Una superficie no tiene profundidad interna.
El caso más frecuente con diferencia es overflow: hidden puesto para redondear esquinas o recortar una imagen. El elemento sigue teniendo transform-style: preserve-3d en su hoja de estilos, DevTools sigue mostrándolo aplicado, y sin embargo el cubo está aplastado. El valor computado sí cambia: si inspeccionas transform-style en el panel de valores computados verás flat, y esa es la prueba definitiva.
flowchart TB
A[El efecto 3D se ve plano] --> B{El ancestro tiene perspective}
B -->|No| C[Falta la camara]
B -->|Si| D{El intermedio tiene preserve-3d declarado}
D -->|No| E[Falta la propagacion]
D -->|Si| F{El valor computado sigue siendo preserve-3d}
F -->|No| G[Alguna propiedad de agrupamiento lo ha forzado a flat]
G --> H[Busca overflow opacity filter clip-path mask blend o contain]
F -->|Si| I{Las caras tienen componente en z}
I -->|No| J[Estan todas en el mismo plano]
I -->|Si| K[La escena es correcta]
style A fill:#f38ba8,color:#11111b
style C fill:#f9e2af,color:#11111b
style E fill:#f9e2af,color:#11111b
style H fill:#f9e2af,color:#11111b
style J fill:#f9e2af,color:#11111b
style K fill:#a6e3a1,color:#11111bbackface-visibility y la tarjeta que se voltea
Cuando una cara gira más de 90 grados, lo que ves es su reverso. Por defecto el reverso se pinta: es el mismo contenido en espejo. backface-visibility: hidden hace que el elemento desaparezca cuando su normal apunta en dirección contraria al observador.
La tarjeta de dos caras es el ejercicio canónico y se resuelve sin ningún truco de opacidad:
<div class="escena">
<div class="tarjeta">
<div class="cara anverso">Anverso</div>
<div class="cara reverso">Reverso</div>
</div>
</div>
.escena { perspective: 1000px; }
.tarjeta {
position: relative;
inline-size: 18rem;
block-size: 24rem;
transform-style: preserve-3d;
transition: rotate .6s;
}
.escena:hover .tarjeta,
.escena:focus-within .tarjeta { rotate: y 180deg; }
.cara {
position: absolute;
inset: 0;
backface-visibility: hidden;
display: grid;
place-items: center;
}
.reverso { rotate: y 180deg; }
@media (prefers-reduced-motion: reduce) {
.tarjeta { transition-duration: 1ms; }
}
Lo que hace que funcione: las dos caras están superpuestas en el mismo sitio, el reverso está girado media vuelta de fábrica, y backface-visibility: hidden garantiza que en cada instante solo una de las dos mira al observador. Sin esa propiedad verías las dos a la vez, la de delante tapando a la de atrás por orden de pintado, y al girar aparecería el texto en espejo.
Ojo con la tentación de redondear las esquinas de .tarjeta con overflow: hidden: eso mata el preserve-3d y la tarjeta deja de voltear. Redondea las caras individualmente con border-radius, que no aplana nada.
Merece la pena entender por qué la especificación prohíbe combinar las dos cosas en lugar de limitarse a lamentarlo. overflow: hidden exige recortar el contenido contra los bordes de una caja en el plano de esa caja. Pero si el subárbol es tridimensional, sus descendientes no están en ese plano: están repartidos por el espacio, algunos delante y otros detrás, y algunos atravesando el plano de recorte en diagonal. Recortar una geometría 3D contra un rectángulo 2D no es una operación bien definida sin decidir antes en qué plano se proyecta —que es justamente lo que significa aplanar—. La misma lógica vale para opacity: aplicar 0,5 de opacidad a un grupo requiere pintar el grupo entero en una superficie aparte y luego mezclarla, porque si no, las partes solapadas se oscurecerían el doble; y esa superficie aparte es, por definición, plana. En otras palabras, cada una de las propiedades de la lista te pide implícitamente que el subárbol sea una imagen, y una imagen no tiene profundidad. Cuando entiendes eso dejas de buscar el flag que lo arregle: no hay flag, hay que reestructurar el árbol y poner el recorte o la opacidad en un elemento que esté fuera del contexto tridimensional, no dentro.
- Monta la tarjeta de dos caras y verifica que el texto del reverso no aparece nunca en espejo.
- Añade
overflow: hiddenal contenedor.tarjetay comprueba en valores computados quetransform-stylepasa aflat. - Repite el experimento con
opacity: .99en lugar deoverflowy confirma que el efecto es el mismo. - Construye un cubo con seis caras y comprueba que sin
preserve-3den el cuerpo se ven todas superpuestas. - Quita
backface-visibility: hiddende las caras del cubo y observa desde dentro cómo se pintan los reversos.