CSS 3D memutar elemen datar di ruang tiga dimensi, lalu menggambarnya seperti dilihat mata dari jarak tertentu. Bayangin kartu kamar di meja resepsionis Bu Sri: depannya foto "Teras pagi", belakangnya daftar fasilitas. Kamu membaliknya seperti membalik kartu sungguhan. Seberapa "dramatis" kartu itu terlihat bergantung pada jarak matamu ke kartu (perspective): dekat, tepinya tampak besar sekali; jauh, nyaris datar. Kartu itu juga punya dua sisi yang direkatkan punggung ke punggung, dan kamu cuma melihat sisi yang menghadapmu (backface-visibility).
🧪 Tebakan cepat: Kartu Kamar Teras lebarnya 320 px, diputar
rotateY(60deg)di dalam induk denganperspective: 800px. Berapa px lebarnya yang terlihat? Jawabannya ada di bagian "Matematika rotateY".
perspective: jarak mata ke layar
.scene { perspective: 800px; perspective-origin: 50% 50%; } /* mata 800 px di depan layar, di tengah */
.flip { transform: rotateY(60deg); } /* anak-anaknya diputar di ruang 3D */
- Properti
perspectivedipasang di induk. Semua anaknya dilihat dari satu mata yang sama, jadi berbagi satu titik hilang (vanishing point).perspective-originmenggeser posisi mata itu; bawaannya50% 50%, tengah induk. - Fungsi
perspective()dipasang di elemen itu sendiri, di awal daftar transform:transform: perspective(800px) rotateY(60deg). Tiap elemen punya mata dan titik hilangnya sendiri. - Untuk satu kartu hasilnya sama (kami ukur: 165 px di keduanya). Untuk tiga kartu berjajar, properti membuat ketiganya miring ke satu titik seperti dilihat satu orang; fungsi membuat tiap kartu tampak dilihat dari depannya masing-masing.
- Makin kecil angkanya, makin dekat matanya, makin kuat distorsinya.
perspective: none= gak ada kedalaman, semuanya tampak datar.
Kartu yang bisa dibalik
<div class="scene">
<div class="flip">
<div class="face depan">Teras pagi</div>
<div class="face belakang">Wi-Fi · Sarapan · Air panas · Parkir motor</div>
</div>
</div>
<button type="button" class="balik" aria-pressed="false">Lihat fasilitas</button>
.scene { perspective: 800px; }
.flip {
position: relative; height: 200px;
transform-style: preserve-3d; /* anak-anaknya ikut di ruang 3D yang sama */
transition: transform 400ms ease-in-out;
}
.flip.dibalik { transform: rotateY(180deg); }
.face { position: absolute; inset: 0; border-radius: 12px; overflow: hidden;
backface-visibility: hidden; } /* sisi yang membelakangi mata disembunyikan */
.belakang { transform: rotateY(180deg); } /* punggung ke punggung dengan sisi depan */
transform-style: preserve-3ddi induk: anak-anaknya berada di ruang 3D yang sama dengan induknya. Tanpa itu, anak-anak digambar rata (flattened) di bidang induknya, dan sisi belakang yang diputar 180° gak pernah ada di belakang.backface-visibility: hiddendi kedua sisi: sisi yang punggungnya menghadap mata disembunyikan. Di 0° yang terlihat sisi depan; lewat 90°, sisi depan membelakangi mata dan hilang, sisi belakang muncul. Tanpa ini, sisi belakang (yang lebih akhir di DOM) menutupi sisi depan bahkan di 0°, terbalik seperti di cermin; kami cek denganelementFromPointdi ketiga mesin browser.- Kartunya dibalik dengan tombol (
aria-pressed), bukan cuma:hover, supaya bisa dipakai di HP dan dengan keyboard. 400 ms = token Material 3medium4, karena kartu satu lebar penuh berputar jauh.
Matematika rotateY
Tanpa perspective, titik yang berjarak x dari sumbu putar tampak di x × cos θ. Kartu 320 px (setengahnya 160 px):
| Sudut | cos θ |
Lebar terlihat |
|---|---|---|
| 0° | 1 | 320 px |
| 60° | 0,5 | 160 px |
| 90° | 0 | 0 px: kartunya jadi garis, saat sisi depan berganti ke belakang |
| 180° | -1 | 320 px lagi, terbalik |
Dengan perspective d, titik yang maju z px ke arah mata tampak dikali d / (d - z). Di 60°, satu tepi maju 160 × sin 60° = 138,6 px dan tepi lainnya mundur sejauh itu:
- tepi dekat: 80 × 800 / (800 - 138,6) = 80 × 1,21 = 96,8 px
- tepi jauh: 80 × 800 / (800 + 138,6) = 80 × 0,85 = 68,2 px
- total: 165 px
Itu jawaban tebakannya: 165 px, sedikit lebih lebar dari 160 px karena tepi yang dekat membesar lebih banyak daripada tepi jauh mengecil. getBoundingClientRect() memberi 165 px di Chromium 147, Firefox 146, dan WebKit 26.4.
Jebakan: preserve-3d yang diam-diam jadi datar
Spesifikasi CSS Transforms 2 menyebut nilai-nilai yang memaksa browser menggambar anak-anak sebagai satu gambar datar dulu, sehingga preserve-3d di elemen itu dipakai sebagai flat: overflow selain visible atau clip, opacity di bawah 1, filter selain none, clip-path, clip, isolation: isolate, mask-image, mask-border-source, mix-blend-mode selain normal, dan contain: paint (termasuk content-visibility: hidden).
Kami taruh label harga dengan translateZ(60px) di dalam kartu. Normalnya label 100 px itu tampak 108,1 px (800 / 740). Dengan overflow: hidden, filter: drop-shadow(...), atau opacity: 0.99 di .flip, lebarnya 100 px di ketiga mesin: kedalamannya hilang. Kartu yang dibalik 180° pun menampilkan sisi depan yang terbalik, bukan fasilitasnya. Jebakannya: getComputedStyle tetap bilang preserve-3d, karena yang berubah nilai pakainya (used value). Pasang overflow, sudut bulat, dan bayangan di .face atau .scene, jangan di .flip.
Performa dan "Kurangi gerakan"
rotateYadalah transform, jadi transition-nya bisa jalan di compositor seperti transform 2D (pelajaran Transform & Composite). Tiap elemen 3D biasanya jadi layer sendiri: tiga kartu gak masalah, ratusan kartu 3D menghabiskan memori GPU.- Putaran besar termasuk gerak yang bisa memicu pusing. Saat "Kurangi gerakan" menyala, ganti putaran dengan crossfade 150 ms:
@media (prefers-reduced-motion: reduce) {
.flip, .flip.dibalik { transform: none; transition: none; }
.belakang { transform: none; opacity: 0; }
.face { transition: opacity 150ms; }
.flip.dibalik .depan { opacity: 0; }
.flip.dibalik .belakang { opacity: 1; }
}
Latihan: membangun string transform
const UNITS = { perspective: 'px', translateX: 'px', translateY: 'px', translateZ: 'px',
rotateX: 'deg', rotateY: 'deg', rotateZ: 'deg', scale: '' };
function buildTransform(transforms) {
return transforms.map((t) => t.type + '(' + t.value + (UNITS[t.type] ?? '') + ')').join(' ');
}
function calculateFlipState(isFlipped, axis) {
return 'rotate' + axis + '(' + (isFlipped ? 180 : 0) + 'deg)';
}
function parseTransformDeg(text, type) {
const match = text.match(new RegExp(type + '\\((-?[\\d.]+)deg\\)'));
return match ? Number(match[1]) : 0;
}
buildTransform([{ type: 'perspective', value: 1000 }, { type: 'rotateY', value: 30 }])=perspective(1000px) rotateY(30deg): jarak pakaipx, sudut pakaideg, dirangkai dengan spasi.calculateFlipState(false, 'X')=rotateX(0deg): nol tetap ditulis lengkap.parseTransformDeg('perspective(800px) rotateY(90deg)', 'rotateY')=90: dicari di dalam teks yang lebih panjang, dan-?menjaga tanda minus (-30).
⚠️ Jebakan yang sering ketemu
transform: perspective()di induk dikira sama dengan propertiperspective. Fungsi itu memiringkan induknya sendiri; anak-anaknya gak mendapat titik hilang bersama.- Lupa
preserve-3d, atau mematikannya tanpa sadar lewatoverflow: hidden,filter, atauopacitydi elemen yang diputar. backface-visibilitycuma di sisi belakang. Pasang di kedua sisi; kalau gak, sisi yang membelakangi mata tetap tergambar terbalik.- Satuan yang salah di latihan.
translateZdanperspectivepakaipx,rotate*pakaideg; angka negatif dan 0 tetap ditulis apa adanya.
TL;DR: Properti perspective di induk = satu mata untuk semua anak (satu titik hilang, digeser dengan perspective-origin); fungsi perspective() di awal transform = mata sendiri untuk tiap elemen. transform-style: preserve-3d membuat anak-anak berada di ruang 3D induknya; backface-visibility: hidden di kedua sisi menyembunyikan sisi yang membelakangi mata; sisi belakang diberi rotateY(180deg), dan kartunya dibalik dengan rotateY(180deg) lewat tombol, 400 ms. Lebar terlihat = lebar × cos θ (60° = 160 px, 90° = garis); dengan perspective d, titik yang maju z dikali d / (d - z), jadi kartu 320 px di 60° dengan 800px tampak 165 px. overflow (selain visible/clip), opacity < 1, filter, clip-path, mask, isolation, mix-blend-mode, dan contain: paint memaksa preserve-3d jadi datar (label translateZ(60px) 108,1 px jadi 100 px, kartu 180° menampilkan depannya terbalik) walau getComputedStyle tetap bilang preserve-3d. Saat "Kurangi gerakan", ganti putaran dengan crossfade 150 ms. Latihan: perspective(1000px) rotateY(30deg), rotateX(0deg), angka diambil kembali dengan regex -?[\d.]+.