Escenas 3D
Tres recetas que usan transforms 3D reales de CSS: un objeto parecido a un teléfono o una tarjeta construido con simples rectángulos que gira para mostrar su parte trasera, tarjetas que voltean alrededor de su eje vertical, y capas de imágenes que cruzan un escenario anclado a distintas velocidades para dar sensación de profundidad.
El 3D de CSS (preserve-3d, backface-visibility) se renderiza plano en algunos navegadores de
pruebas automatizadas, incluso cuando es correcto. Comprueba siempre los efectos 3D reales en
Chrome, Firefox o Safari de verdad, no solo en una ejecución de pruebas automatizada.
Objeto 3D con grosor real
Un teléfono, tarjeta o caja hecho enteramente de bloques simples (frente, parte trasera, bordes, esquinas redondeadas) que gira en 3D genuino mientras la página hace scroll — no se necesitan imágenes del objeto en sí. Un ejemplo típico: un teléfono que gira desde su parte trasera hasta su frente, y luego se inclina para encarar cada función a medida que el visitante avanza con el scroll.
Estructura (dentro de una escena anclada; la cara frontal debe ser el primer hijo y la cara trasera el segundo — la geometría de los bordes depende de ese orden):
div .tour el único elemento animado — "Get parent" + Custom, Start 20, End 80
└─ div .device perspective; tamaño general del objeto
└─ div .device__spin preserve-3d; gira de atrás hacia adelante al principio mismo de la escena
└─ div .device__body preserve-3d; hace la inclinación; position: relative
├─ div .device__front hijo 1
├─ div .device__back hijo 2
├─ div .device__side ×4 hijos 3–6: los cuatro bordes rectos
├─ div .device__corner ×36 hijos 7–42: 9 tiras por esquina redondeada
└─ div .device__core último hijo
El patrón CSS principal:
.tour {
--dv-w: min(17rem, 34vh);
--dv-spin: clamp(0, var(--fa-scroll--custom, 1) * 6, 1); /* gira de la parte trasera a la frontal en la primera sexta parte de la fila */
--dv-a: -35deg; /* el ángulo de inclinación — cualquier expresión del progreso del scroll */
}
.device { width: var(--dv-w); aspect-ratio: 1 / 2.05; position: relative; perspective: calc(var(--dv-w) * 6); }
.device__spin { width: 100%; height: 100%; transform-style: preserve-3d; rotate: y calc((1 - var(--dv-spin, 1)) * -180deg); }
.device__body {
width: 100%; height: 100%; position: relative; transform-style: preserve-3d; rotate: y var(--dv-a, 0deg);
--dv-t: calc(var(--dv-w) * .09); /* grosor */
--dv-te: calc(var(--dv-t) + 2px); /* bordes ligeramente más gruesos, para que queden escondidos detrás de cada cara */
--dv-r: calc(var(--dv-w) * .16); /* radio de la esquina */
}
.device__front, .device__back, .device__core { position: absolute; inset: 0; border-radius: var(--dv-r); }
.device__front, .device__back { backface-visibility: hidden; -webkit-backface-visibility: hidden; }
.device__front { transform: translateZ(calc(var(--dv-t) / 2)); background: #141417; }
.device__back { transform: rotateY(180deg) translateZ(calc(var(--dv-t) / 2)); background: #2a2a2f; }
La construcción completa (los cuatro bordes rectos, las 36 tiras de esquina con sus ángulos de rotación individuales, y la variante «inclinarse hacia cada función en sincronía con una secuencia») es una pieza de CSS larga y precisa, basada en trigonometría, reproducida por completo en el propio contenido de referencia de abilities del plugin. Las partes que conviene conocer de antemano:
- 36 tiras de esquina, 9 por cada esquina redondeada en incrementos de 10°, cada una con su
propio
left/top/rotateZderivado de una única fórmula (solo cambian el signo y el ángulo entre ellas) — tiras planas por sí solas dejan huecos visibles, y menos de 9 por esquina se ve facetado en vez de redondeado. - Los hijos deben ordenarse exactamente así: frente (1), parte trasera (2), los 4 lados (3–6), las
36 tiras de esquina (7–42), y luego un núcleo sólido como el último hijo de todos — nada debería
desplazar las reglas
:nth-childpensadas para las esquinas. - Inclinarse hacia una función usa las mismas matemáticas continuas de «qué elemento está centrado» que la receta de secuencia: el ángulo de inclinación del objeto se convierte en una función de qué leyenda de función está centrada en cada momento, en lugar de saltar entre ángulos fijos.
Valores que puedes cambiar: --dv-w (tamaño general), las proporciones del grosor (.09 ×
anchura) y del radio de esquina (.16 × anchura), el multiplicador de perspectiva (más bajo = un
efecto 3D más fuerte y dramático), los colores de cara/borde, y cuánto de la fila de scroll ocupa el
giro inicial (* 6 = la primera sexta parte).
- Bricks
- Etch
- Tour: "Get parent" + una fila Custom, Start 20, End 80. Nada dentro del objeto en sí necesita ningún ajuste de Fancy — solo lo necesita el único controlador exterior.
- Cada pieza vacía (lados, esquinas, núcleo) necesita un ancho/alto mínimo explícito de 0 (y outline en none) — de lo contrario, el constructor da a los bloques vacíos un marcador de posición de 90×90px, que deforma el objeto solo en el lienzo (no en el frontend).
- Clases del tour:
fa--parent fa--custom fa--custom--start-20 fa--custom--end-80 fa--smooth-10(da a cualquier leyenda de función el mismo valor de suavizado). - El lienzo muestra los elementos vacíos como cajas de marcador de posición — fija a 0 la variable
de tamaño de elemento vacío solo del lienzo en la clase de
.device__bodypara que no distorsione la forma mientras editas.
Problemas comunes:
- El objeto gira completamente plano, sin ninguna sensación de profundidad (en sitios anteriores a
3.1.1). Las versiones antiguas mantenían un
filter: opacity(1)en cada elemento animado, y cualquier filtro CSS aplanapreserve-3d— asegúrate de que ninguna clase de Fancy se sitúe entre.devicey las caras; solo el único controlador exterior que calcula las variables debería llevar uno. Esto está corregido desde 3.1.1 en adelante. - Huecos visibles, o un aspecto facetado en vez de redondeado, en las esquinas. Demasiadas pocas tiras por esquina — usa 9 por esquina, cada 10°, superponiéndose ligeramente, no 3.
- «Costuras» finas de luz visibles en Safari y Firefox. El anti-aliasing en los bordes deja que se transparente la página — haz que los bordes sean 2px más gruesos que el propio cuerpo, con un núcleo oscuro sólido como el último hijo de todos.
- Un borde recto desaparece por completo en Chrome. Chrome no pinta un
background-imageen un elemento alto y estrecho rotado 90° — pon el degradado en un pseudoelemento::beforedentro de la tira del borde en su lugar. - El 3D se ve completamente plano durante las pruebas automatizadas. Algunos navegadores de
pruebas headless renderizan
preserve-3dplano incluso cuando es correcto — compruébalo en un navegador real.
Tarjetas que voltean en 3D
Tres tarjetas se deslizan y giran hasta colocarse en su sitio, y luego voltean alrededor de su eje vertical para revelar su parte trasera — todo impulsado por el scroll, dentro de un escenario anclado.
Estructura (una escena anclada de 200vh, anclada entre 33–67%):
div .pin__stage
└─ div .flip__grid 3 columnas (1 en móviles)
└─ div .flip tarjetas laterales: "Get parent" + Custom 35–45, Inverted (las mueve a su sitio)
├─ div .flip__front "Get parent" + Custom 50–60
│ └─ div .flip__depth > contenido
└─ div .flip__back "Get parent" + Custom 50–60, Inverted
└─ div .flip__depth > contenido
Cada tarjeta lleva dos ventanas de scroll separadas a propósito: la fila propia de la tarjeta la mueve hasta su sitio, y las filas propias de las dos caras gestionan el volteo. Un hijo con su propia fila Custom siempre lee su propio progreso, no el de su padre.
CSS:
.flip {
position: relative; min-height: 450px;
transform-style: preserve-3d; perspective: 1000px;
}
.flip:nth-child(1) { transform: translateX(calc(var(--fa-scroll--custom, 0) * 60%)) rotate(calc(var(--fa-scroll--custom, 0) * -15deg)); }
.flip:nth-child(3) { transform: translateX(calc(var(--fa-scroll--custom, 0) * -60%)) rotate(calc(var(--fa-scroll--custom, 0) * 15deg)); }
.flip__front, .flip__back {
transform-style: preserve-3d;
backface-visibility: hidden; -webkit-backface-visibility: hidden;
border-radius: 1rem; padding: 1.5rem;
display: flex; flex-direction: column; justify-content: center; align-items: center;
}
.flip__front {
position: absolute; inset: 0; background: #fff;
transform: rotateY(calc(var(--fa-scroll--custom, 0) * -180deg));
}
.flip__back {
position: relative; height: 100%; z-index: -1; background: #27272a; color: #fff;
transform: rotateY(calc(var(--fa-scroll--custom, 0) * 180deg));
}
.flip__depth { transform: translateZ(80px) scale(.9); }
La fila de la cara trasera está invertida (Inverted) (va de 1 → 0), así que empieza girada hacia atrás a 180° y termina de cara al visitante, mientras el frente gira de 0° a -180° al mismo tiempo. La tarjeta central no tiene ninguna fila propia en absoluto — ya está en su posición final.
Valores que puedes cambiar: las dos ventanas (colocación en su sitio 35–45, volteo 50–60 —
mantén ambas dentro de la ventana anclada), perspective (más bajo = 3D más fuerte),
translateZ(80px) en .flip__depth (a qué distancia flota el contenido sobre cada cara), y el
desplazamiento (60%) e inclinación (15deg) de las tarjetas laterales.
- Bricks
- Etch
- Tarjetas laterales: "Get parent" + Custom, Start 35, End 45, Inverted.
- Frente: Custom, Start 50, End 60. Trasera: lo mismo, también Inverted. Ambas "Get parent".
- "Disable from mobile" en tarjetas y caras, y presenta las caras planas (apiladas, no volteadas) por debajo de 768px.
- Tarjetas laterales:
fa--parent fa--custom fa--custom--start-35 fa--custom--end-45 fa--custom--inv. - Frente:
fa--parent fa--custom fa--custom--start-50 fa--custom--end-60; trasera: lo mismo másfa--custom--inv. - Móviles:
fa--no-mobileen tarjetas y caras, con el maquetado plano anidado en@media (max-width: 767px).
Problemas comunes:
- Ambas caras son visibles a la vez, o el texto se lee reflejado. Falta
backface-visibility: hidden(con el prefijo-webkit-para Safari) en una de las caras. - No hay sensación de profundidad en el contenido — las caras se ven planas (en sitios anteriores
a 3.1.1). El mismo problema de
filter: opacity(1)que en la receta del objeto 3D anterior — esto necesita 3.1.1; el volteo en sí funciona bien en 3.0 sin la capa de profundidad. - El volteo se reproduce mientras la tarjeta todavía se está deslizando a su sitio. Las ventanas del movimiento propio de la tarjeta y del volteo de sus caras se solapan — deja un hueco entre ellas (por ejemplo, 45 → 50).
- El 3D se ve plano durante las pruebas automatizadas. Compruébalo en un navegador real en su lugar.
Capas de paralaje
Dos usos de la misma idea subyacente: la misma ventana de scroll, aplicada a distintas distancias, se percibe como profundidad.
A. Capas que cruzan un escenario anclado
Dentro de una escena anclada (una escena de 300vh, anclada entre 25–75%), un título centrado se queda quieto mientras dos cuadrículas de imágenes se elevan pasando junto a él — una capa «frontal» y una capa «trasera», la trasera más lenta, con menos contraste y colocada detrás del título.
div .pin__stage align-items: center
├─ div .layers__content el título, z-index 2
├─ div .layers__front "Get parent" + Custom, Start 10, End 70, From 5
│ └─ img .layers__image …
└─ div .layers__back "Get parent" + Custom, End 70
└─ img .layers__image …
.layers__content { position: relative; z-index: 2; text-align: center; }
.layers__front, .layers__back {
position: absolute; top: 0;
display: grid; grid-template-columns: 1fr 1fr;
}
.layers__front { z-index: 1; width: calc(100% - 3rem);
transform: translateY(calc(85% + var(--fa-scroll--custom) * -180%)); }
.layers__back { z-index: -1; width: 70%; opacity: .4;
transform: translateY(calc(85% + var(--fa-scroll--custom) * -120%)); }
.layers__image { width: 100%; max-width: 23vw; aspect-ratio: 3 / 4; object-fit: cover; }
.layers__image:nth-child(even) { margin-left: auto; }
Ambas capas empiezan un 85% de su propia altura por debajo de su borde superior; la capa frontal recorre un 180% de su altura, la trasera solo un 120% — la diferencia entre esos dos números es lo que se percibe como profundidad. Escalona las imágenes con márgenes alternos para que la cuadrícula no se vea demasiado regular.
B. Una imagen de fondo que se desplaza dentro de una sección normal
section .bg-parallax "I am parent", position: relative
├─ div .bg-parallax__content
└─ div .bg-parallax__window absolute, inset 0, overflow hidden, z-index -1
└─ img .bg-parallax__image "Get parent" + Custom (sin Start/End)
.bg-parallax { position: relative; z-index: 1; }
.bg-parallax__window { position: absolute; inset: 0; z-index: -1; overflow: hidden; }
.bg-parallax__image {
position: absolute; left: 0; width: 100%; height: 200%;
object-fit: cover; border-radius: 0;
top: calc(var(--fa-scroll--custom, 0) * -100%);
}
La imagen mide el doble de la altura propia de la sección y se mueve hacia arriba exactamente una
altura de sección mientras la sección cruza la pantalla. Para la dirección opuesta, intercambia
top/bottom: top: auto; bottom: calc(var(--fa-scroll--custom, 0) * -100%);.
Valores que puedes cambiar: A — los dos porcentajes de recorrido (una diferencia mayor entre
ellos se percibe como más profundidad), la opacidad y el ancho de la capa trasera. B — la altura
extra (200%, combinada con un recorrido de -100%) y la distancia de recorrido (mantén ambas a
juego, o aparecerá un hueco arriba o abajo).
- Bricks
- Etch
- A: las capas frontal y trasera reciben cada una "Get parent" + su propia fila Custom con las ventanas de arriba.
- B: la sección recibe "I am parent"; la imagen recibe "Get parent" + una fila Custom sin ventana, además de Object fit nativo "cover" y border radius 0.
- Los valores de rotación van en el CSS personalizado, no en los campos de rotación del control de
transform nativo — esos no conservan las expresiones
calc().
- A: frontal
fa--parent fa--custom fa--custom--start-10 fa--custom--end-70 fa--custom--from-5; traserafa--parent fa--custom fa--custom--end-70. - B: sección
fa--iparent; imagenfa--parent fa--custom.
Problemas comunes:
- Solo se ve la capa frontal — ninguna sensación de profundidad. Las capas son fotos opacas a sangre completa, así que la frontal simplemente cubre la trasera. Usa imágenes con transparencia real, o enmascara cada capa más cercana para que solo se transparente su parte inferior.
- Un título hero no se mueve en absoluto con el scroll. En su lugar tiene una animación de entrada, que se reproduce una vez al cargar y luego se detiene — dale a su contenedor envolvente una fila Custom dentro de la ventana anclada en su lugar.
- Un fondo redondeado o con bandas tipo letterbox (variante B). Muchos temas redondean por
defecto cada
img— fijaborder-radius: 0yobject-fit: coverdirectamente en la imagen. - La capa no se mueve en absoluto. Falta "Get parent", o falta "I am parent" por encima de ella.
- Un preajuste en el mismo elemento cancela el transform. Zoom, Rotate y los preajustes de slide
también escriben
transform— mantén una sola fuente de transform por elemento.