CSS 動態背景、3D 翻轉卡片、翻頁、抽獎滾輪:四個效果一次學,手機和減少動態都顧到

CSS 動態背景、3D 翻轉卡片、翻頁、抽獎滾輪:四個效果一次學,手機和減少動態都顧到

最後更新:2026/10/15

網站做好了,老闆看著首頁說「好像有點死」。他想要的多半是這幾樣:主視覺的背景會慢慢流動、服務的卡片滑過去會翻面、作品集有翻書的感覺、週年慶活動頁有一個抽獎的動畫。這些效果都有外掛,但每裝一個外掛就多一包程式碼,而且常常不能改。

這四個效果用純 CSS 加一小段 JavaScript 就能做,這篇每一個都給程式碼,說明關鍵的幾行在做什麼,最後講一個很多網站漏掉的事:使用者在系統裡設了「減少動態效果」時,動畫要停下來。四個效果我們都在 Chrome 用電腦和手機寬度測過。

一、會流動的漸層背景

.hero {
    position: relative;
    overflow: hidden;
    min-height: 320px;
    display: flex;
    align-items: center;
    justify-content: center;
    text-align: center;
    color: #fff;
    background: linear-gradient(120deg, #ffb703, #fb8500, #e63946, #8338ec);
    background-size: 300% 300%;
    animation: mimi-flow 12s ease-in-out infinite;
}
@keyframes mimi-flow {
    0%   { background-position: 0% 50%; }
    50%  { background-position: 100% 50%; }
    100% { background-position: 0% 50%; }
}
/* 幾顆慢慢飄的圓 */
.hero .blob {
    position: absolute;
    width: 220px; height: 220px;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.18);
    filter: blur(2px);
    animation: mimi-float 9s ease-in-out infinite;
}
.hero .blob:nth-child(2) { left: 10%; top: 10%; }
.hero .blob:nth-child(3) { right: 8%; bottom: -40px; width: 300px; height: 300px; animation-delay: -4s; }
@keyframes mimi-float {
    0%, 100% { transform: translateY(0); }
    50%      { transform: translateY(-30px); }
}
.hero h1 { position: relative; font-size: clamp(1.6rem, 4vw, 2.6rem); margin: 0; text-shadow: 0 2px 12px rgba(0,0,0,.25); }

HTML 是一個 .hero 區塊,裡面放三個 <span class="blob"> 和一個 <h1>。原理:漸層背景做成畫面的三倍大(background-size: 300%),再用動畫慢慢移動背景的位置,看起來顏色就在流動。幾顆半透明的圓用另一個動畫上下飄,animation-delay 設成負數讓它們不要同步。顏色換成你網站的配色,挑法在〈網站配色怎麼選〉。12 秒一個循環是刻意放慢的,流動太快會讓人頭暈。

二、3D 翻轉卡片

.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 20px; max-width: 760px; }
.flip { perspective: 1000px; height: 180px; }
.flip-inner {
    position: relative; width: 100%; height: 100%;
    transform-style: preserve-3d;
    transition: transform .6s;
}
.flip:hover .flip-inner, .flip:focus-within .flip-inner, .flip.is-flipped .flip-inner { transform: rotateY(180deg); }
.face {
    position: absolute; inset: 0;
    display: flex; align-items: center; justify-content: center;
    padding: 16px; border-radius: 16px;
    backface-visibility: hidden;   /* 背面轉過來時,正面不要透出來 */
    border: 2px solid #222;
    background: #fff7d6;
    font-weight: 700;
}
.face.back { transform: rotateY(180deg); background: #222; color: #ffb703; font-weight: 400; }
.flip button { position: absolute; inset: 0; opacity: 0; width: 100%; height: 100%; cursor: pointer; }

每張卡片的 HTML 是:.flip 裡放 .flip-inner,裡面兩個 .face(正面和 .face.back),最後一顆看不見的 <button> 蓋在整張卡上。三個關鍵:外層的 perspective 給 3D 的景深;transform-style: preserve-3d 讓正反兩面在 3D 空間裡各自存在;backface-visibility: hidden 讓轉到背面時正面不會透出來。背面一開始就先轉 180 度,整張卡轉 180 度時,背面剛好正對你。

手機沒有「滑鼠移上去」,所以多了那顆透明的按鈕:按一下加上 is-flipped 類別就翻面,鍵盤的使用者用 Tab 到按鈕也會翻(:focus-within)。

document.querySelectorAll('.flip button').forEach(function (b) {
    b.addEventListener('click', function () { b.parentElement.classList.toggle('is-flipped'); });
});

三、3D 翻頁

.book { perspective: 1400px; width: min(100%, 520px); height: 240px; position: relative; margin: 0 auto; }
.page {
    position: absolute; left: 50%; top: 0; width: 50%; height: 100%;
    transform-origin: left center;   /* 以書脊為軸轉 */
    transform-style: preserve-3d;
    transition: transform .9s cubic-bezier(.3,0,.2,1);
    cursor: pointer;
}
.page-face {
    position: absolute; inset: 0; padding: 20px; box-sizing: border-box;
    border: 2px solid #222; background: #fff; backface-visibility: hidden;
}
.page-face.back { transform: rotateY(180deg); background: #fff7d6; }
.book.is-open .page { transform: rotateY(-180deg); }
.book .cover-left { position: absolute; left: 0; top: 0; width: 50%; height: 100%; border: 2px solid #222; box-sizing: border-box; background: #e8f3ec; padding: 20px; }

翻頁和翻卡片是同一個原理,差別在轉軸:卡片以中心轉,書頁以左邊(書脊)轉,所以 transform-origin: left center,而且往負的方向轉 180 度,右頁就翻到左邊。點一下整本書加上 is-open 類別就翻過去,再點一下翻回來。這是最簡單的單頁翻書,多頁的翻書要用 JavaScript 管每一頁的順序和層次,那就超出這篇的範圍。

四、抽獎滾輪

.drum { width: min(100%, 320px); height: 64px; overflow: hidden; border: 3px solid #222; border-radius: 14px; background: #fff; position: relative; margin: 0 auto 12px; }
.drum::before, .drum::after { content: ""; position: absolute; left: 0; right: 0; height: 18px; z-index: 1; pointer-events: none; }
.drum::before { top: 0; background: linear-gradient(#fff, rgba(255,255,255,0)); }
.drum::after { bottom: 0; background: linear-gradient(rgba(255,255,255,0), #fff); }
.drum ul { list-style: none; margin: 0; padding: 0; transition: transform 3s cubic-bezier(.1,.8,.2,1); }
.drum li { height: 64px; line-height: 64px; text-align: center; font-size: 1.4rem; font-weight: 700; }
.drum.is-done li.winner { color: #e63946; }
.btn { display: block; margin: 0 auto; padding: 10px 24px; border: 2px solid #222; border-radius: 999px; background: #ffb703; font-weight: 700; cursor: pointer; }
(function () {
    var names = ['小美', '阿哲', '雅婷', '俊宏', '佳蓉', '志明', '怡君', '家豪'];
    var ul = document.getElementById('names'), drum = document.getElementById('drum'), btn = document.getElementById('draw');
    var rows = [];
    for (var r = 0; r < 6; r++) { names.forEach(function (n) { rows.push(n); }); }
    ul.innerHTML = rows.map(function (n) { return '<li>' + n + '</li>'; }).join('');
    btn.addEventListener('click', function () {
        drum.classList.remove('is-done');
        ul.querySelectorAll('li').forEach(function (li) { li.classList.remove('winner'); });
        var pick = Math.floor(Math.random() * names.length);          // 抽中第幾個人
        var index = names.length * 4 + pick;                          // 先轉 4 圈再停
        ul.style.transition = 'none'; ul.style.transform = 'translateY(0)';
        void ul.offsetHeight;                                         // 讓瀏覽器先畫一次歸零
        ul.style.transition = '';
        ul.style.transform = 'translateY(-' + index * 64 + 'px)';
        ul.children[index].classList.add('winner');
        setTimeout(function () { drum.classList.add('is-done'); btn.textContent = '抽中:' + names[pick] + '(再抽一次)'; }, 3000);
    });
})();

滾輪是一個高度固定、超出的部分藏起來的框(.drum),裡面一個很長的名單,每一列 64 像素高。按下按鈕時,程式隨機挑一個人,把名單往上推到那個人剛好在框裡的位置:名單先複製六次,推的距離是「轉四圈再加抽中的位置」,用 3 秒的緩動讓它先快後慢停下來,上下兩道漸層遮罩做出滾筒的感覺,停下來的那一格變紅色。要用在真的活動上,名單從後台或表單匯入,抽中的人記得另外存起來,動畫只是給現場看的。

五、尊重「減少動態效果」

有些人看動畫會暈,手機和電腦的系統都有「減少動態效果」的設定,網站要聽它的話:

@media (prefers-reduced-motion: reduce) {
    .hero, .hero .blob { animation: none; }
    .flip-inner, .page, .drum ul { transition: none; }
}

這一段放在所有動畫的最後面:使用者設了減少動態,背景不流動、圓不飄、卡片和書頁直接翻到位、滾輪直接跳到結果,功能都在,只是沒有過程。我們在 Chrome 模擬這個設定測過,四個效果的動畫都停了。這是一個不花力氣但很多網站沒做的事。

測試結果

我們把四個效果放在同一頁,在 Chrome 用 1280 和 390 兩種寬度測:背景動畫和飄動都在跑,沒有橫向捲動;按卡片會翻面、按書會翻頁、按抽獎會轉三秒停在抽中的人上;開「減少動態效果」之後,背景動畫變成無、翻轉的過渡時間變成 0 秒。

效果要為內容服務

流動的背景是為了讓主視覺的標題被看到,翻面的卡片是為了讓說明在需要時才出現,抽獎動畫是為了活動的氣氛。效果本身不是目的,一頁放超過兩種動態效果,客人的注意力就散了。按鈕和文字的小動畫在〈CSS 文字動畫與按鈕效果〉,區塊之間的斜角和波浪分隔在〈CSS 區塊分隔線〉,三篇合起來,首頁要的動態差不多都夠了。

如果你想要一個有適度動態、而且在手機上一樣順的網站,可以先看〈IG 帳號不是你的資產〉想想網站要做到什麼。MIMI 專做客製化網頁設計,首頁的動態效果會依品牌設計,並確保手機和減少動態的使用者都能正常看。MIMI 提供台灣網頁設計服務,全台灣的店家都可以用電話、Email 直接和設計師討論;費用可以先看首頁的製作費用參考,歡迎聯絡 MIMI。

網站圖示怎麼放:Font Awesome、SVG、WordPress 內建 Dashicons,選單加圖示和小字

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

我的車
願望清單
最近瀏覽過的
類別