商用網站架設實務

LESSON-06 · checkpoint

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

主要結構已經完成,只留下本堂一個核心缺口。先預測,再從指定檔案找出它。 這頁用 可觀察證據anchor 指出下一個動作。

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

開始前確認

這一頁的結果:入口頁可以開啟

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

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

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

照這個順序做

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

CHECKPOINT-1

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

目的

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

修改檔案

style.css

程式碼位置

--muted 對比缺口

現在要做:請在 style.css 的「--muted 對比缺口」位置完成:找出低對比的 --muted,將它改成在白色背景上至少 4.5:1 的文字色

貼上位置:style.css:--muted 對比缺口 區塊,先保留同一檔案的其他內容。

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

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

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

卡住時先看這裡

常見錯誤:只改 --text,卻沒有檢查真正使用 --muted 的段落;或只看 token 宣告沒有看實際元素。

第一個檢查:只選取 .page-description,記下 Computed colorbackground-color,再檢查 --muted 是否被 var() 使用。

階段檔案

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

開啟 checkpoint 的可執行入口

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 · checkpoint</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: #8a98a8;
/* CHECKPOINT-1:故意保留低對比的 --muted,請以 Computed 與對比檢查找出缺口。 */
  --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);

完成驗收

下一步

Checkpoint 完成後,先說出你修正的原因,再前往 Solution 對照每個檔案差異。

前往 Solution,逐檔對照差異