商用網站架設實務

LESSON-06 · solution

CSS 基礎:字型、顏色與背景

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

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

開始前確認

這一頁的結果:所有相關檔案可開啟或閱讀;沒有 TODOCHECKPOINT 或 placeholder;能說明每個檔案為何改動

工作方式:先把這一階段複製到自己的工作資料夾,開啟 files/index.html,記下修改前的畫面、DOM、Console 或 Network狀態。

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

本堂焦點:完成一張服務型商家卡片的字型、文字階層、色彩、背景與窄版可讀性

照這個順序做

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

差異 1

讓學生先把字體、大小、粗細與行距拆成四個可以觀察的 CSS property。

目的

讓學生先把字體、大小、粗細與行距拆成四個可以觀察的 CSS property

修改檔案

style.css

程式碼位置

.service-card typography

現在要做:先對照差異,再檢查 在 style.css 的「.service-card typography」對照這項變更:.service-card h2 { margin-block: 0 0 .75rem; font-size: clamp(1.5rem, 5vw, 2.25rem); font-weight: 800; line-height: 1.2; } .service-card p { line-height: 1.6; max-width: 62ch; }

貼上位置:style.css.service-card typography 區塊

.service-card h2 {
  margin-block: 0 0 .75rem;
  font-size: clamp(1.5rem, 5vw, 2.25rem);
  font-weight: 800;
  line-height: 1.2;
}
.service-card p {
  line-height: 1.6;
  max-width: 62ch;
}

預期結果:卡片標題較突出,段落有至少 1.5 的 line-height,長文字在窄版仍容易閱讀。

驗收證據:選取 .service-card h2 與 .service-card p,在 Computed 讀到 font-familyfont-sizefont-weightline-height

卡住時先看這裡

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

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

差異 2

把可重複的顏色集中成 CSS custom properties,讓一次修改可以觀察多個元件同步變化。

目的

把可重複的顏色集中成 CSS custom properties,讓一次修改可以觀察多個元件同步變化。

修改檔案

style.css

程式碼位置

:root tokens 與 var()

現在要做:先對照差異,再檢查 在 style.css 的「:root tokens 與 var()」對照這項變更::root { --brand: #176b87; --text: #17324d; --muted: #52657d; --surface: #ffffff; --on-brand: #ffffff; --page-bg: #f5f8fb; --focus: #1d4ed8; } body { background-color: var(--page-bg); color: var(--text); } button { background-color: var(--brand); color: var(--on-brand); }

貼上位置:style.css 的 :root tokens 與 var() 區塊

:root {
  --brand: #176b87;
  --text: #17324d;
  --muted: #52657d;
  --surface: #ffffff;
  --on-brand: #ffffff;
  --page-bg: #f5f8fb;
  --focus: #1d4ed8;
}
body { background-color: var(--page-bg); color: var(--text); }
button { background-color: var(--brand); color: var(--on-brand); }

預期結果:修改 --brand 後 eyebrow 與按鈕同步變色;修改 --text 或 --muted 後對應文字同步更新。

驗收證據:Styles 看到 var(--brand) 或 var(--muted),Computed 顯示最後的 rgb 值;不要只看 :root 宣告。

卡住時先看這裡

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

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

差異 3

把背景和操作狀態分開檢查,避免只追求品牌色而犧牲可用性。

目的

把背景和操作狀態分開檢查,避免只追求品牌色而犧牲可用性。

修改檔案

style.css

程式碼位置

background 與 focus-visible

現在要做:先對照差異,再檢查 在 style.css 的「background 與 focus-visible」對照這項變更:.service-card { background-color: var(--surface); } button:focus-visible, a:focus-visible { outline: 3px solid var(--focus); outline-offset: 3px; }

貼上位置:style.css 的 background 與 focus-visible 區塊

.service-card {
  background-color: var(--surface);
}
button:focus-visible, a:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 3px;
}

預期結果:卡片與頁面背景有層次;按 Tab 時 focus 線清楚且不使用白底上約 1.98:1 的亮黃色。

驗收證據:Computed 讀到 background-color;鍵盤 Tab 可看到 focus-visiblefocus 色在白底上至少達到 UI 檢查的 3:1 基準。

卡住時先看這裡

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

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

差異 4

讓學生以實際計算與畫面證據修正 token,而不是只相信 Console 宣稱成功。

目的

讓學生以實際計算與畫面證據修正 token,而不是只相信 Console 宣稱成功。

修改檔案

style.css

程式碼位置

--muted 對比缺口

現在要做:先對照差異,再檢查 在 style.css 的「--muted 對比缺口」對照這項變更::root { --muted: #52657d; }

貼上位置:style.css 的 --muted 對比缺口 區塊

:root { --muted: #52657d; }

預期結果:段落與輔助文字的 Computed color 不再是低對比值,白底一般文字達到至少 4.5:1。

驗收證據:先重現 #8a98a8 在白底約 2.94:1,再把 --muted 改成 #52657d 並重新讀取 Computed對比結果。

卡住時先看這裡

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

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

階段檔案

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

開啟 solution 的可執行入口

index.html入口檔案:提供一張可觀察字型、色彩與背景的服務卡片。

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

<!doctype html>
<html lang="zh-Hant">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='8' fill='%23176b87'/%3E%3C/svg%3E">
  <title>CSS 基礎:字型、顏色與背景|服務卡片練習</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="page-shell">
    <p class="eyebrow">LESSON-06 · solution</p>
    <h1>讓服務內容容易讀懂</h1>
    <p class="page-description">本堂只完成一張服務型商家卡片的字型、文字階層、色彩、背景與窄版可讀性。</p>
    <article class="service-card" aria-labelledby="service-title">
      <h2 id="service-title">定期維護</h2>
      <p class="service-description">每月整理一次環境,讓商家可以把時間留給真正重要的工作。</p>
      <button id="action" type="button">執行 CSS 驗收</button>
      <p id="status" class="status" role="status">尚未執行 CSS 驗收。</p>
    </article>
  </main>
  <script src="main.js" defer></script>
</body>
</html>
style.css呈現層:依 StarterCheckpointSolution 展示本堂 CSS 差異。

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

:root {
  color-scheme: light;
  --page-bg: #f5f8fb;
  --surface: #ffffff;
  --text: #17324d;
  --muted: #52657d;
  --brand: #176b87;
  --on-brand: #ffffff;
  --border: #cbd8e4;
  --focus: #1d4ed8;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100vh;
  background-color: var(--page-bg);
  color: var(--text);
  font-family: system-ui, -apple-system, "Segoe UI", "Noto Sans TC", sans-serif;
  line-height: 1.7;
}

.page-shell {
  width: min(100% - 2rem, 680px);
  margin: 2rem auto;
  padding: 2rem;
}

.eyebrow { color: var(--brand); font-weight: 800; letter-spacing: .08em; }
.service-description { color: var(--muted); }
.service-card {
  margin-top: 2rem;
  padding: 1.25rem;
  border: 1px solid var(--border);
  border-radius: 1rem;
  background-color: var(--surface);
}
.service-card h2 {
  margin-block: 0 .75rem;
  font-size: clamp(1.5rem, 5vw, 2.25rem);
  font-weight: 800;
  line-height: 1.2;
}
.service-card p { max-width: 62ch; line-height: 1.6; }
button {
  padding: .7rem 1rem;
  border: 0;
  border-radius: .6rem;
  background-color: var(--brand);
  color: var(--on-brand);
  font: inherit;
  cursor: pointer;
}
button:focus-visible, a:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 3px;
}
.status { min-height: 2rem; color: var(--muted); }

@media (max-width: 390px) {
  .page-shell { margin: 1rem auto; padding: 1rem; }
}
main.js驗收輔助:按鈕後讀取 Computed Style、計算對比並列出尚未通過條件。

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

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

function parseRgb(value) {
  const match = value.match(/rgba?\((\d+)[, ]+(\d+)[, ]+(\d+)/);
  return match ? match.slice(1, 4).map(Number) : null;
}

function relativeLuminance([red, green, blue]) {
  const channels = [red, green, blue].map((channel) => channel / 255).map((channel) => (
    channel <= 0.03928 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4
  ));
  return 0.2126 * channels[0] + 0.7152 * channels[1] + 0.0722 * channels[2];
}

function contrastRatio(foreground, background) {
  if (!foreground || !background) return 0;
  const lighter = Math.max(relativeLuminance(foreground), relativeLuminance(background));
  const darker = Math.min(relativeLuminance(foreground), relativeLuminance(background));
  return (lighter + 0.05) / (darker + 0.05);
}

function readStylesheetText() {
  return Array.from(document.styleSheets).flatMap((sheet) => {
    try { return Array.from(sheet.cssRules).map((rule) => rule.cssText); } catch { return []; }
  }).join("\n");
}

function auditCss() {
  const card = document.querySelector(".service-card");
  const heading = document.querySelector(".service-card h2");
  const description = document.querySelector(".service-description");
  const button = document.querySelector("#action");
  if (!status || !card || !heading || !description || !button) {
    if (status) status.textContent = "CSS 輔助檢查無法開始:先確認指定元素存在。";
    return;
  }

  const headingStyle = getComputedStyle(heading);
  const descriptionStyle = getComputedStyle(description);
  const cardStyle = getComputedStyle(card);
  const buttonStyle = getComputedStyle(button);
  const stylesheetText = readStylesheetText();
  const textContrast = contrastRatio(parseRgb(descriptionStyle.color), parseRgb(cardStyle.backgroundColor));
  const buttonContrast = contrastRatio(parseRgb(buttonStyle.color), parseRgb(buttonStyle.backgroundColor));
  const report = {
    fontFamily: headingStyle.fontFamily,
    fontSize: headingStyle.fontSize,
    fontWeight: headingStyle.fontWeight,
    lineHeight: headingStyle.lineHeight,
    textColor: descriptionStyle.color,
    cardBackground: cardStyle.backgroundColor,
    textContrast: Number(textContrast.toFixed(2)),
    buttonContrast: Number(buttonContrast.toFixed(2)),
    usesBrandToken: stylesheetText.includes("var(--brand)"),
    usesMutedToken: stylesheetText.includes("var(--muted)"),
    hasFocusRule: stylesheetText.includes(":focus-visible") && stylesheetText.includes("var(--focus)") && !stylesheetText.includes("outline: none"),
    javascriptExecuted: true,
  };
  const failures = [];
  if (headingStyle.lineHeight === "normal" || !stylesheetText.includes(".service-card h2")) failures.push("文字階層");
  if (!report.usesBrandToken || !report.usesMutedToken) failures.push("token/var() 使用");
  if (textContrast < 4.5) failures.push("一般文字對比");
  if (buttonContrast < 4.5) failures.push("按鈕文字對比");
  if (!report.hasFocusRule) failures.push("鍵盤 focus");
  const passed = failures.length === 0;
  status.textContent = passed
    ? "JavaScript 已執行;CSS 條件通過,請再用畫面、DevTools 與 390px viewport 交叉確認。"
    : `JavaScript 已執行;CSS 已讀取,但仍有未通過項目:${failures.join("、")}。`;
  console.log("LESSON-06 CSS audit", { passed, report, failures });
}

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

完成驗收

下一步

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

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