商用網站架設實務

LESSON-06 · starter

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

你會依序完成3個本堂專屬微步驟;每一步只修改一個責任,看到結果後才進下一步。 這頁用 可觀察證據anchor 指出下一個動作。

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

開始前確認

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

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

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

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

照這個順序做

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

TODO-1

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

目的

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

修改檔案

style.css

程式碼位置

.service-card typography

現在要做:請在 style.css 的「.service-card typography」位置完成:替服務卡片補上 font-familyfont-sizefont-weightline-height 的文字階層

貼上位置: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

卡住時先看這裡

常見錯誤:一次貼上很多顏色或版面規則,導致不知道是哪個 property 造成變化。

第一個檢查:只檢查 style.css 是否真的有 .service-card h2 與 .service-card p,接著重新整理讀取 Computed

TODO-2

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

目的

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

修改檔案

style.css

程式碼位置

:root tokens 與 var()

現在要做:請在 style.css 的「:root tokens 與 var()」位置完成:建立 --brand、--text、--muted、--surface、--on-brand、--page-bg 與 --focus,並在實際 selector 使用 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); }

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

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

卡住時先看這裡

常見錯誤:只宣告 --brand 卻沒有用 var(--brand),或把 CSS custom property 寫成 JavaScript const

第一個檢查:先搜尋 style.css 的 --brand 與 var(--brand),再選取按鈕查看 Computed background-color

TODO-3

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

目的

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

修改檔案

style.css

程式碼位置

background 與 focus-visible

現在要做:請在 style.css 的「background 與 focus-visible」位置完成:使用背景 token 與較深的 focus token,讓按鈕和鍵盤焦點清楚可辨

貼上位置: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 基準。

卡住時先看這裡

常見錯誤:只用滑鼠測試,或用太亮的 focus 顏色讓鍵盤使用者看不到位置。

第一個檢查:按 Tab 到按鈕,只檢查焦點是否可見,再查看按鈕的 Computed background-color

階段檔案

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

開啟 starter 的可執行入口

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 · starter</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 {
  font-family: system-ui, -apple-system, "Segoe UI", "Noto Sans TC", sans-serif;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100vh;
  background-color: #f5f8fb;
  color: #17324d;
  line-height: 1.7;
}

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

.eyebrow { color: #176b87; font-weight: 800; letter-spacing: .08em; }
.service-description { color: #52657d; }
.service-card {
  margin-top: 2rem;
  padding: 1.25rem;
  border: 1px solid #cbd8e4;
  border-radius: 1rem;
  background-color: #ffffff;
}
button {
  padding: .7rem 1rem;
  border: 0;
  border-radius: .6rem;
  background-color: #176b87;
  color: #ffffff;
  font: inherit;
  cursor: pointer;
}
.status { min-height: 2rem; color: #17643d; }

/* TODO-1:補上 .service-card h2 與 .service-card p 的文字階層。 */
/* TODO-2:建立可重複的顏色名稱,並套用到實際元件。 */
/* TODO-3:補上背景層次與 button:focus-visible。 */

@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);

完成驗收

下一步

Starter 完成後,不要直接看 Solution;先前往 Checkpoint,自己找出最後一個核心缺口。

前往 Checkpoint,自己補最後一個核心缺口