商用網站架設實務

LESSON-13 · solution

CSS 進階:Transition、動畫與樣式整理

這一頁不是直接複製答案;先逐檔看差異、修改原因與驗收證據,再把需要的改動帶回自己的資料夾。 這頁用 可觀察證據anchor 指出下一個動作。

Starter3 個小成果
Checkpoint一個核心缺口
Solution4 個差異

開始前確認

這一頁的結果:按鈕與卡片的 hover 進出、Tab focus 都有清楚回饋;正常偏好與 reduced motion 的 transform/animation 有可觀察差異;390px 寬度沒有水平捲軸,且 solution 無 TODOCHECKPOINT 或已驗證假訊息

工作方式:先把這一階段複製到自己的工作資料夾,開啟 files/index.html,記下修改前的畫面、Elements、Styles、Computed、Tab 與 Rendering狀態。

會用到的檔案:index.htmlstyle.cssmain.js

本堂焦點:class狀態transition 做出可驗收的按鈕與服務卡片回饋

照這個順序做

一次只做一張卡。完成後先重新整理入口,再留下證據,才進入下一張。

差異 1

建立 HTML 元素與 CSS selector 之間可以被檢查的對應關係。

目的

建立 HTML 元素與 CSS selector 之間可以被檢查的對應關係。

修改檔案

index.html

程式碼位置

#action

現在要做:先對照差異,再檢查 在 index.html 的「#action」對照這項變更:<button id="action" class="primary-button" type="button">查看推薦方案</button>

貼上位置:index.html 的 #action 區塊

<button id="action" class="primary-button" type="button">查看推薦方案</button>

預期結果:Elements 中的按鈕有 primary-button,兩張卡片都有 service-card;此時尚未期待動畫。

驗收證據:Elements 搜尋 .primary-button 與 .service-card,數量和 HTML 結構一致。

卡住時先看這裡

常見錯誤:只複製完整答案,卻沒有先理解這一項差異的責任。

第一個檢查:只對照目前差異指定的檔案與 anchor。

差異 2

分清楚 transition 需要前後狀態,而 animation 可以依 keyframes 自己播放。

目的

分清楚 transition 需要前後狀態,而 animation 可以依 keyframes 自己播放。

修改檔案

style.css

程式碼位置

.primary-button、.service-card 與 @keyframes attention

現在要做:先對照差異,再檢查 在 style.css 的「.primary-button、.service-card 與 @keyframes attention」對照這項變更:.primary-button { transition: transform 200ms ease, background-color 200ms ease, box-shadow 200ms ease; } .service-card { transition: transform 200ms ease, box-shadow 200ms ease; } @keyframes attention { from { opacity: .4; transform: translateY(-4px); } to { opacity: 1; transform: translateY(0); } }

貼上位置:style.css 的 .primary-button、.service-card 與 @keyframes attention 區塊

.primary-button {
  transition: transform 200ms ease, background-color 200ms ease, box-shadow 200ms ease;
}

.service-card {
  transition: transform 200ms ease, box-shadow 200ms ease;
}

@keyframes attention {
  from { opacity: .4; transform: translateY(-4px); }
  to { opacity: 1; transform: translateY(0); }
}

預期結果:按鈕和卡片的 hover、focus-within 有平順變化;推薦標籤只播放一次短 animation。

驗收證據:hover 進入與移出、Tab focusStyles 命中;Computed 看到 transform、background-color、box-shadow 的 transition

卡住時先看這裡

常見錯誤:只複製完整答案,卻沒有先理解這一項差異的責任。

第一個檢查:只對照目前差異指定的檔案與 anchor。

差異 3

尊重使用者希望減少動態的偏好,同時保留清楚的鍵盤 focus。

目的

尊重使用者希望減少動態的偏好,同時保留清楚的鍵盤 focus

修改檔案

style.css

程式碼位置

@media (prefers-reduced-motion: reduce)

現在要做:先對照差異,再檢查 在 style.css 的「@media (prefers-reduced-motion: reduce)」對照這項變更:@media (prefers-reduced-motion: reduce) { .primary-button { transition: background-color .01ms linear; } .service-card { transition: none; } .service-card__tag { animation: none; } .primary-button:hover, .primary-button:focus-visible, .service-card:hover, .service-card:focus-within { transform: none; box-shadow: none; } }

貼上位置:style.css@media (prefers-reduced-motion: reduce) 區塊

@media (prefers-reduced-motion: reduce) {
  .primary-button { transition: background-color .01ms linear; }
  .service-card { transition: none; }
  .service-card__tag { animation: none; }
  .primary-button:hover,
  .primary-button:focus-visible,
  .service-card:hover,
  .service-card:focus-within { transform: none; box-shadow: none; }
}

預期結果:切換 reduce 後 transform 位移不再出現、animation 不播放或被縮短,outline 仍清楚。

驗收證據:Rendering 的 prefers-reduced-motionComputedtransition 與 Tab focus 對照正常/reduce 兩種狀態

卡住時先看這裡

常見錯誤:只複製完整答案,卻沒有先理解這一項差異的責任。

第一個檢查:只對照目前差異指定的檔案與 anchor。

差異 4

修正只有移入會平滑、移出卻瞬間跳回的真實缺口。

目的

修正只有移入會平滑、移出卻瞬間跳回的真實缺口。

修改檔案

style.css

程式碼位置

.primary-button:hover

現在要做:先對照差異,再檢查 在 style.css 的「.primary-button:hover」對照這項變更:.primary-button { transition: transform 200ms ease, background-color 200ms ease, box-shadow 200ms ease; } .primary-button:hover, .primary-button:focus-visible { transform: translateY(-2px); }

貼上位置:style.css 的 .primary-button:hover 區塊

.primary-button {
  transition: transform 200ms ease, background-color 200ms ease, box-shadow 200ms ease;
}

.primary-button:hover,
.primary-button:focus-visible {
  transform: translateY(-2px);
}

預期結果:移入和移出都平順;一般 selectorComputed 可讀到 transition狀態 selector 只保留變化後的值。

驗收證據:先觀察移入/移出差異,再在 Styles 找到 transition 的實際位置,最後用 Computed 檢查 propertyduration、timing function

卡住時先看這裡

常見錯誤:只複製完整答案,卻沒有先理解這一項差異的責任。

第一個檢查:只對照目前差異指定的檔案與 anchor。

階段檔案

完整檔案收在可展開的卡片中;先完成上方微步驟,再用這裡查閱與複製。檔案位置要和目前的相對路徑一致。

開啟 solution 的可執行入口

index.html入口檔案:建立 primary-button、service-card 與可操作的服務方案示範。

這段要貼在哪裡:先在自己的工作資料夾建立同名相對位置,再複製內容。

<!doctype html>
<html lang="zh-Hant">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>LESSON-13|Transition 與動畫練習</title>
  <link rel="icon" href="../../../assets/favicon.svg" type="image/svg+xml">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <header class="site-header"><span class="brand">安心維護</span><span class="lesson-label">LESSON-13 · CSS 狀態實驗</span></header>
  <main class="page-shell">
    <p class="eyebrow">LESSON-13 · transition lab</p>
    <h1>讓服務卡片的狀態變化更容易理解</h1>
    <p class="page-description">用滑鼠與鍵盤觀察狀態,再用 Styles、Computed 與 Rendering 找到證據。</p>
    <section class="demo-panel" aria-labelledby="demo-title">
      <h2 id="demo-title">服務方案預覽</h2>
      <div class="service-list">
        <article class="service-card service-card--featured">
          <span class="service-card__tag">推薦服務</span>
          <h3>定期維護</h3>
          <p>每月一次的檢查與整理,讓訪客知道下一步可以怎麼開始。</p>
          <button id="action" class="primary-button" type="button">查看推薦方案</button>
        </article>
        <article class="service-card">
          <span class="service-card__tag">入門服務</span>
          <h3>到府諮詢</h3>
          <p>先把需求說清楚,再選擇適合的服務方式。</p>
          <p class="card-note">本卡片用來觀察 service-card 的共用規則。</p>
        </article>
      </div>
      <p id="status" class="status" role="status">尚未操作。先移入按鈕,或按 Tab 觀察 focus。</p>
    </section>
    <aside class="evidence-note"><strong>這堂只看:</strong>class 對應、hover/focus 狀態、transition、animation 與 reduced motion。</aside>
  </main>
  <script src="main.js" defer></script>
</body>
</html>
style.css呈現層:集中放置狀態transition、animation 與 reduced-motion 規則。

這段要貼在哪裡:先在自己的工作資料夾建立同名相對位置,再複製內容。

:root { font-family: system-ui, -apple-system, "Segoe UI", "Noto Sans TC", sans-serif; color: #17324d; background: #f5f8fb; }
* { box-sizing: border-box; }
body { margin: 0; min-height: 100vh; line-height: 1.7; }
.site-header { width: min(100% - 2rem, 920px); margin: 1rem auto 0; display: flex; flex-wrap: wrap; justify-content: space-between; gap: .5rem 1rem; align-items: center; }
.brand { color: #155e75; font-weight: 800; }
.lesson-label, .page-description, .card-note { color: #52657d; }
.page-shell { width: min(100% - 2rem, 860px); margin: 2rem auto; }
.eyebrow { color: #176b87; font-weight: 800; letter-spacing: .06em; }
.demo-panel { padding: clamp(1rem, 3vw, 1.5rem); border: 1px solid #cbd8e4; border-radius: 20px; background: #fff; }
.service-list { display: flex; flex-wrap: wrap; gap: 1rem; }
.service-card { flex: 1 1 16rem; min-width: 0; padding: 1.25rem; border: 1px solid #d8e0ea; border-radius: 16px; background: #fbfdff; }
.service-card__tag { display: inline-block; color: #176b87; font-weight: 800; }
.service-card h3 { margin: .35rem 0; }
.primary-button, button { padding: .7rem 1rem; border: 0; border-radius: 10px; background: #176b87; color: #fff; font: inherit; cursor: pointer; }
.primary-button:hover, .primary-button:focus-visible { background-color: #0f5268; box-shadow: 0 8px 16px rgb(23 107 135 / 24%); transform: translateY(-2px); }
.primary-button:focus-visible { outline: 3px solid #f3b63f; outline-offset: 3px; }
.service-card:hover, .service-card:focus-within { box-shadow: 0 12px 24px rgb(23 50 77 / 14%); transform: translateY(-2px); }
.status { min-height: 2rem; color: #17643d; }
.evidence-note { margin-top: 1rem; padding: .9rem 1rem; border-left: 4px solid #f3b63f; background: #fff8e6; }

.primary-button { transition: transform 200ms ease, background-color 200ms ease, box-shadow 200ms ease; }
.service-card { transition: transform 200ms ease, box-shadow 200ms ease; }
@keyframes attention {
  from { opacity: .4; transform: translateY(-4px); }
  to { opacity: 1; transform: translateY(0); }
}
.service-card__tag { animation: attention 900ms ease both; }

@media (prefers-reduced-motion: reduce) {
  .primary-button { transition: background-color .01ms linear; }
  .service-card { transition: none; }
  .service-card__tag { animation: none; }
  .primary-button:hover,
  .primary-button:focus-visible,
  .service-card:hover,
  .service-card:focus-within { transform: none; box-shadow: none; }
}
main.js行為層:只處理按鈕操作後的狀態提示,不假裝驗證 CSS

這段要貼在哪裡:先在自己的工作資料夾建立同名相對位置,再複製內容。

const status = document.querySelector("#status");
const action = document.querySelector("#action");

function showResult() {
  if (!status) return;
  status.textContent = "按鈕已被操作。請回到 Styles 與 Computed 檢查狀態規則。";
  console.log("LESSON-13:按鈕操作已執行");
}

if (action) action.addEventListener("click", showResult);

完成驗收

下一步

Solution 對照完成後,回到 README 整理證據,並準備下一堂課的先備知識。

回到 README,整理本堂證據並銜接下一堂