昨天做佈告欄時,已經先用了一點 3D:perspective() 搭配 rotateX(),讓紙片被掀起時有立體感。今天就把 CSS 3D 完整拆開來,先認識 CSS 3D 的幾個關鍵屬性,再用它做出「翻牌」動畫。那我們開始吧!
可以把 CSS 3D 想像成一個拍攝現場:
transform)。perspective)。preserve-3d)。backface-visibility)。下面逐一說明。
2D 只有 X(左右)、Y(上下)兩個方向,3D 則多了 Z 軸,代表朝向或遠離螢幕的方向。要注意 CSS 的 Y 軸是往下為正。

對應的轉換函式有:
| 類型 | 函式 |
|---|---|
| 旋轉 | rotateX()、rotateY()、rotateZ()、rotate3d() |
| 位移 | translateZ()、translate3d() |
| 縮放 | scaleZ()、scale3d() |
rotateX():繞著水平軸轉,像「前後翻」。rotateY():繞著垂直軸轉,像「左右開門」。rotateZ():繞著 Z 軸轉,其實就是 2D 的 rotate()。角度的正負值,依 MDN 的說明,正值是順時針、負值是逆時針。但要特別注意,這個「順時針」是從該軸的正端朝原點看的方向,不是從螢幕正面看。

perspective 立體感的來源只對元素做 rotateY(60deg),看起來只是被橫向壓扁,因為沒有「遠近」的概念,近的邊和遠的邊一樣大。
這時就需要 perspective,它決定的是「鏡頭離畫面有多遠」:

perspective 有屬性與函式兩種寫法:
/* 寫在父層:所有子層共用同一個消失點 */
.stage {
perspective: 900px;
}
/* 寫在自己:只影響自己,要放在 transform 最前面(昨天用的就是這種) */
.card {
transform: perspective(900px) rotateY(60deg);
}
鏡頭的位置預設在中心,若想調整,可以使用 perspective-origin 屬性改變鏡頭的位置(消失點)。
preserve-3d 讓子層保有 3Dperspective 只會對「直接子層」生效,不會被繼承下去。如果子層裡面還有孫層,預設的 transform-style: flat 會把孫層壓平在子層的平面上,看不出各自的 3D 位置。
設成 preserve-3d 之後,孫層才會跟著待在同一個 3D 空間裡。
.parent { perspective: 900px; }
.child {
transform-style: preserve-3d; /* 孫層才能保有 3D */
}
💡
preserve-3d要「一層一層」連續設定。中間任何一層沒設,畫面一樣會被壓扁。另外,部分屬性(例如overflow不是visible、opacity小於 1、filter等)也會讓preserve-3d失效,完整清單請參考 MDN。
preserve-3d 會讓瀏覽器多做 3D 的處理,遇到效能問題時,可以在不需要立體的地方維持預設的 flat。
backface-visibility 背面要不要顯示元素轉到背對螢幕時,預設會看到「鏡像的背面」,也就是反著的圖像。設成 hidden,背對螢幕時就直接不顯示。
.face {
backface-visibility: hidden;
}
這就是翻牌效果的核心:把兩個面疊在一起,兩面都設成「背對時不顯示」,轉到哪一面,就只會看到哪一面。
畫面是一排牌卡,hover 時會稍微靠近鏡頭。原本是牌背朝上,點擊牌卡後,沿著垂直軸翻轉,露出牌面;再點一次就翻回去。
<div class="flip-card">
<div class="flip-card-inner">
<div class="flip-card-back">牌背</div>
<div class="flip-card-front">牌面</div>
</div>
</div>
三層各司其職:
.flip-card:加上 perspective,提供立體空間(鏡頭)。.flip-card-inner:加上 transform-style: preserve-3d,並負責旋轉。.flip-card-back、.flip-card-front:牌的正反兩個面,用 position: absolute 疊在一起,並設定 backface-visibility: hidden。💡 注意
perspective下在被旋轉者的父層。因為perspective屬性是作用在子元素身上,如果和rotateY放在同一個元素上,這個元素自己的旋轉就不會有透視效果。
.flip-card {
width: 200px;
aspect-ratio: 5 / 7;
perspective: 900px;
}
.flip-card-inner {
position: relative;
width: 100%;
height: 100%;
transform-style: preserve-3d;
transition: transform 0.35s ease;
}
.flip-card:hover .flip-card-inner {
transform: translateZ(30px);
}
.flip-card.is-flipped .flip-card-inner {
transform: rotateY(180deg);
}
.flip-card.is-flipped:hover .flip-card-inner {
transform: translateZ(30px) rotateY(180deg);
}
.flip-card-back,
.flip-card-front {
position: absolute;
inset: 0;
backface-visibility: hidden;
}
.flip-card-front {
transform: rotateY(180deg); /* 預先翻到背後 */
}
用一點 JavaScript(或 Vue 的 :class)切換 is-flipped:
document.querySelectorAll('.flip-card').forEach(card => {
card.addEventListener('click', () => card.classList.toggle('is-flipped'));
});
重點在 .flip-card-front 這一行 transform: rotateY(180deg)。牌面一開始就先轉到背後,加上 backface-visibility: hidden,所以一開始看到的是牌背。當 .flip-card-inner 轉了 180 度,牌背轉到後面被隱藏,原本在後面的牌面剛好轉到前面。
其他小細節:
aspect-ratio 固定長寬比,兩個面都用 inset: 0 撐滿,才能完全疊合。transition,而不是 @keyframes,因為翻牌是「狀態 A ↔ 狀態 B」的來回切換,這種情況用 transition 最適合。這樣,一張可以翻轉的牌就完成了!

其實翻牌也可以只用 scaleX 做到:讓牌在水平方向從 1 縮到 0,換成另一個面,再從 0 放大回 1。不過因為沒有視覺上的透視感,比起被翻過去,更像被壓扁、拉伸。想要更像真實的翻牌,還是要用 CSS 3D。
今天用了 perspective、preserve-3d 製作出有立體感的翻轉動畫,讓網頁的動態效果更加吸睛。