# Day 58-59：CSS 重構實作：DOM 佈局空間管理、BEM 命名方法、圖片容器問題

By [雞蛋糕的前端修煉屋](https://paragraph.com/@gcake) · 2026-02-12

---

學習背景
----

**重構時間**：2026 年 2 月  
**重構範圍**：ZERO TYPE Landing Page (index.html + style.css)  
**技術限制**：尚未使用 Flexbox/Grid，僅使用傳統 Box Model + Position 定位  
**學習目標**：建立可維護、可預測的版面結構與命名系統

* * *

Part 1: DOM 佈局與元素空間管理
---------------------

### 為什麼需要空間管理原則？

當使用傳統 Box Model 建立版面時，常見問題：

*   圖片載入前後版面高度跳動
    
*   圖片破圖或尺寸過大導致版面崩潰
    
*   修改內容後其他元素位置連帶跑掉
    
*   不同瀏覽器或載入速度下版面不一致
    

**核心問題**：沒有明確定義「誰負責版面空間」、「誰負責填充內容」。

* * *

原則 1：父層元素「切出空間」，不靠子元素推擠
-----------------------

### 問題場景：依賴子元素推擠

    <!-- ❌ 錯誤：沒有定義父層高度 -->
    <div class="hero">
      <img src="box.png" />
    </div>
    

    .hero {
      position: relative;
      /* height 沒設定，由子元素決定 */
    }
    
    .hero img {
      width: 600px;
    }
    

**執行流程問題**：

1.  瀏覽器遇到 `.hero`，不知道高度，先畫 0px
    
2.  遇到 `<img>`，等待圖片下載
    
3.  圖片下載完成 400px
    
4.  `.hero` 被撐高到 400px，**整個版面重排（reflow）**
    

**結果**：版面跳動，使用者體驗差

* * *

### 解決方式：父層明確定義空間

    <!-- ✅ 正確：用容器切出空間 -->
    <section class="hero">
      <div class="hero__img-box">
        <img class="hero__img" src="box.png" />
      </div>
    </section>
    

    /* 父層明確定義版面高度 */
    .hero {
      position: relative;
      height: 450px;  /* 明確告訴瀏覽器：這區塊佔 450px */
      margin-bottom: 250px;
    }
    
    /* 圖片容器：定義它在父層中的位置和寬度 */
    .hero__img-box {
      position: absolute;
      width: 45%;
      top: 50%;
      right: 0;
      transform: translateY(-50%);
    }
    
    /* 圖片：填滿容器即可 */
    .hero__img {
      width: 100%;
      height: 100%;
    }
    

**改善後的執行流程**：

1.  瀏覽器遇到 `.hero`，立刻畫出 450px 高的區塊
    
2.  遇到 `.hero__img-box`，用 `absolute` 定位在內部
    
3.  圖片下載完成，填入容器
    
4.  **不需要 reflow，版面穩定**
    

* * *

### 實際案例：Header Logo 區域

    <div class="header__logo-zone">
      <div class="header__logo">
        <img class="header__logo-img" src="./public/logo.png" alt="網站 logo" />
      </div>
      <p class="header__logo-text">ZERO TYPE</p>
    </div>
    

    /* 父層：定義整個 logo 區域的大小和位置 */
    .header__logo-zone {
      display: inline-block;
      margin: 15px 80px 30px 150px;
      text-align: center;
    }
    
    /* 圖片容器：切出 60px 寬的空間給 logo */
    .header__logo {
      width: 60px;
      margin: 0 auto;
    }
    
    /* 圖片：填滿固定空間 */
    .header__logo-img {
      width: 100%;
      height: auto;
    }
    

**空間結構圖**：

    ┌─ .header__logo-zone ──────────────────┐
    │  [上 padding 15px]                     │
    │                                        │
    │  ┌─ .header__logo (60px) ───────┐   │
    │  │   <img> 填滿這個 60px 的空間   │   │
    │  └────────────────────────────────┘   │
    │                                        │
    │  ZERO TYPE (文字)                      │
    └────────────────────────────────────────┘
    

**關鍵思考**：

*   `logo-zone`：控制整個區域的 margin 和對齊
    
*   `logo`：切出圖片固定的 60px 空間
    
*   `img`：只負責填滿那 60px
    

* * *

原則 2：子元素視覺與尺寸都不超過父元素
--------------------

### 問題場景：子元素超出父層

    <div class="container">
      <img src="large-image.png" />
    </div>
    

    .container {
      width: 300px;
    }
    
    .container img {
      /* 沒有設定 width，圖片以原始尺寸顯示 */
      /* 假設原始尺寸是 800px，會超出 container */
    }
    

**問題視覺化**：

    ┌─ .container (300px) ──┐
    │                        │
    │ ┌─ <img> 800px ────────────────────────┐
    │ │                      │                │
    │ └──────────────────────┘────────────────┘
    └────────────────────────┘
              ↑
         超出 500px
    

* * *

### 解決方式：子元素尺寸相對於父層

    .container {
      width: 300px;
    }
    
    .container img {
      width: 100%;     /* 永遠不會超過 300px */
      height: auto;    /* 保持圖片比例 */
    }
    

**執行結果**：

    ┌─ .container (300px) ──┐
    │ ┌─ <img> (300px) ───┐ │
    │ │   圖片被縮小到     │ │
    │ │   符合容器寬度     │ │
    │ └────────────────────┘ │
    └────────────────────────┘
    

* * *

### 實際案例：Hero 主視覺圖片

**版面需求**：

*   Hero 區塊總高度 450px
    
*   左側文字佔 50% 寬度
    
*   右側圖片佔 45% 寬度
    
*   圖片垂直置中
    

    /* 第一層：父層定義整體空間 */
    .hero {
      position: relative;
      height: 450px;
      margin-bottom: 250px;
    }
    
    /* 第二層：圖片容器定義圖片應佔的空間 */
    .hero__img-box {
      position: absolute;
      width: 45%;
      top: 50%;
      right: 0;
      transform: translateY(-50%);
    }
    
    /* 第三層：圖片填滿容器 */
    .hero__img {
      width: 100%;
      height: 100%;
    }
    

**空間結構圖**：

    ┌─ .hero (height: 450px) ────────────────────────┐
    │                                                 │
    │  ┌─ .hero__intro ──┐  ┌─ .hero__img-box ────┐│
    │  │ (width: 50%)    │  │ (width: 45%)         ││
    │  │ IDEAS?          │  │  ┌─ <img> ────────┐ ││
    │  │ This is just... │  │  │   填滿容器      │ ││
    │  │ [TRY IT NOW!]   │  │  │   100% × 100%  │ ││
    │  └─────────────────┘  │  └─────────────────┘ ││
    │                       └──────────────────────┘│
    └─────────────────────────────────────────────────┘
    

* * *

原則 3：圖片等空元素用 `<div>` 包裹，避免破圖跑版
------------------------------

### 核心概念：Replaced Element 的特殊性

`<img>` 是 **replaced element**（置換元素），它的特性： [developer.mozilla](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_images/Replaced_element_properties)

*   內容由外部資源決定（圖片檔案）
    
*   尺寸預設由圖片原始大小決定
    
*   載入狀態會影響版面（載入前/後/失敗）
    

**其他 replaced elements**：`<video>`, `<iframe>`, `<embed>`, `<object>` [github](https://github.com/mdn/content/blob/main/files/en-us/web/css/css_images/replaced_element_properties/index.md?plain=1)

* * *

### 問題場景：直接對圖片定位

    <!-- ❌ 錯誤：直接對 img 使用 absolute -->
    <div class="hero">
      <img class="hero__img" src="box.png" />
    </div>
    

    .hero {
      position: relative;
      /* 沒有定義高度 */
    }
    
    .hero__img {
      position: absolute;
      width: 45%;
      right: 0;
      top: 50%;
      transform: translateY(-50%);
    }
    

**問題 1：圖片載入前**

    ┌─ .hero (高度 0px) ─┐
    │                     │  ← 沒設定高度，且 img 還沒載入
    └─────────────────────┘
    下方內容緊貼著
    

**問題 2：圖片載入後**

    ┌─ .hero (被撐高到 300px) ─────┐
    │         ┌─ img (300px) ─────┐│
    │         │                    ││
    │         └────────────────────┘│
    └───────────────────────────────┘
    下方內容被推下 300px (版面跳動！)
    

**問題 3：圖片載入失敗**

    ┌─ .hero (高度又變回 0px) ─┐
    │ [破圖 icon]              │
    └──────────────────────────┘
    版面又跳回去 (再次跳動！)
    

* * *

### 解決方式：用 div 切出空間，圖片填充

    <!-- ✅ 正確：三層結構 -->
    <div class="hero">                    <!-- 1. 版面骨架 -->
      <div class="hero__img-box">         <!-- 2. 圖片空間 -->
        <img class="hero__img" src="box.png" />  <!-- 3. 圖片內容 -->
      </div>
    </div>
    

    /* 第一層：版面骨架 */
    .hero {
      position: relative;
      height: 450px;
    }
    
    /* 第二層：圖片空間切割 */
    .hero__img-box {
      position: absolute;
      width: 45%;
      top: 50%;
      right: 0;
      transform: translateY(-50%);
    }
    
    /* 第三層：圖片填充 */
    .hero__img {
      width: 100%;
      height: 100%;
    }
    

**載入流程分析**：

✅ **圖片載入前/後/失敗都穩定**：

    ┌─ .hero (height: 450px) ───────────────┐
    │                                        │
    │  ┌─ .hero__img-box ─────────────────┐│
    │  │  [載入中 / 顯示圖片 / 破圖 icon] ││
    │  │  但 .hero 始終保持 450px 高度    ││
    │  └───────────────────────────────────┘│
    │                                        │
    └────────────────────────────────────────┘
    版面大小不變，沒有跳動
    

* * *

### 為什麼要分三層？責任分離原則

#### 層級 1：`.hero`（版面骨架層）

**責任**：定義整個區塊在頁面中的空間

    .hero {
      position: relative;  /* 成為子元素定位的參考點 */
      height: 450px;       /* 佔據固定高度，穩定版面 */
      margin-bottom: 250px;
    }
    

#### 層級 2：`.hero__img-box`（空間切割層）

**責任**：在骨架內部切出「圖片應該佔的位置和大小」

    .hero__img-box {
      position: absolute;
      width: 45%;
      top: 50%;
      right: 0;
      transform: translateY(-50%);
    }
    

#### 層級 3：`.hero__img`（內容填充層）

**責任**：把圖片內容填入預留的空間

    .hero__img {
      width: 100%;   /* 填滿父層 */
      height: 100%;
    }
    

* * *

Part 2: CSS 命名結構重構 - BEM 方法
---------------------------

### BEM 核心概念

**BEM = Block\_\_Element--Modifier** [getbem](https://getbem.com)

    Block__Element--Modifier
    

*   **Block（區塊）**：獨立的頁面元件
    
    *   例：`.header`, `.hero`, `.footer`
        
*   **Element（元素）**：Block 的組成部分，用 `__` 連接 [hackmd](https://hackmd.io/@YIHQx96xTI-K9vDjhzEfDA/S1TBmnon9)
    
    *   例：`.header__logo`, `.header__nav`, `.footer__copyright`
        
*   **Modifier（修飾符）**：Block 或 Element 的不同狀態，用 `--` 連接 [hackmd](https://hackmd.io/@taiwansmile/HJeRAbahhC)
    
    *   例：`.button--disabled`, `.nav-item--active`
        

* * *

### 判斷是否需要加 class 的流程

    這個元素需要 CSS 樣式嗎？
      ├─ 是 → 加 class
      └─ 否
          └─ 這個元素需要 JS 操作嗎？
              ├─ 是 → 加 class
              └─ 否 → 不用加 class（純語意容器）
    

### Block 還是 Element？決策流程

    這個元件會在多個地方重用嗎？
      ├─ 是 → 獨立成 Block (.button, .card, .modal)
      └─ 否 → 當作 Element (.hero__button, .footer__social-links)
    

* * *

### 實際重構案例 1：Header 區塊

**Before:**

    <header>
      <div class="logo">
        <div class="logo-img"><img src="..." /></div>
        <p class="logo-text">ZERO TYPE</p>
      </div>
      <nav>
        <ul class="header-nav">
          <li><span class="home-btn">Home</span></li>
    

    header { ... }               /* ❌ tag selector */
    .logo { ... }                /* ❌ 沒有明確從屬關係 */
    header nav { ... }           /* ❌ 後代選擇器 */
    .header-nav li { ... }       /* ❌ 選擇器層級過深 */
    

**After:**

    <header class="header">
      <div class="header__logo-zone">
        <div class="header__logo">
          <img class="header__logo-img" src="..." />
        </div>
        <p class="header__logo-text">ZERO TYPE</p>
      </div>
      <nav>
        <ul class="header__nav">
          <li class="header__nav-item">Home</li>
    

    .header { ... }
    .header__logo-zone { ... }
    .header__logo { ... }
    .header__logo-img { ... }
    .header__nav-item { ... }
    .header__nav-item:hover { color: orange; }
    

**改善重點**：

*   移除 tag selector，全部用 class
    
*   移除後代選擇器 `.logo-img img`
    
*   用 `__` 明確表達從屬關係 [hackmd](https://hackmd.io/@YIHQx96xTI-K9vDjhzEfDA/S1TBmnon9)
    

* * *

### Modifier 的正確用法

**錯誤寫法**：

    <li class="header__nav-home">Home</li>
    <li class="header__nav-list">Feature</li>
    

這樣看起來像兩個不同的元素，而非同一元素的不同狀態。

**正確寫法**：

    <li class="header__nav-item header__nav-item--active">Home</li>
    <li class="header__nav-item">Feature</li>
    

    .header__nav-item {
      color: gray;
      list-style-type: none;
      display: inline-block;
      margin-right: 100px;
    }
    
    .header__nav-item--active {
      color: orange;
    }
    

**為什麼要這樣寫**： [hackmd](https://hackmd.io/@chupai/SkeNVToGr)

*   所有選單項目共用 `.header__nav-item` 的基礎樣式
    
*   只有被選中的項目額外加上 `--active` 的顏色
    
*   未來修改「所有選單」或「選中狀態」都只改一個 class
    

* * *

### 重構案例 2：Hero 區塊（複雜結構）

**Before:**

    <section class="hero">
      <div class="hero-intro">
        <h1>IDEAS?</h1>
        <h2>This is just a placeholder.</h2>
        <button class="try-it-btn">
          <h2>TRY IT NOW!</h2>
        </button>
      </div>
      <div class="hero-img">
        <img src="..." />
      </div>
    </section>
    

    section.hero .hero-intro h1 { ... }  /* ❌ 4層選擇器 */
    .hero-img img { ... }                /* ❌ 後代選擇器 */
    

**After:**

    <section class="hero">
      <div class="hero__intro">
        <h1 class="hero__title">IDEAS?</h1>
        <h2 class="hero__subtitle">This is just a placeholder.</h2>
        <p class="hero__text">...</p>
        <div class="hero__try-it">
          <button class="hero__try-it-btn">TRY IT NOW!</button>
          <p class="hero__try-it-hint">Don't worry it's for free.</p>
        </div>
      </div>
      <div class="hero__img-box">
        <img class="hero__img" src="..." />
      </div>
    </section>
    

    .hero__title { font-size: 50px; ... }
    .hero__subtitle { font-size: 30px; ... }
    .hero__try-it-btn { ... }
    .hero__img { width: 100%; height: 100%; }
    

**改善重點**：

1.  所有標題標籤都加上語意化 class
    
2.  按鈕從獨立 Block 改為 `.hero__try-it`
    
3.  圖片容器和圖片分別命名
    
4.  移除所有 4 層以上的選擇器 [hackmd](https://hackmd.io/@chupai/SkeNVToGr)
    

* * *

### 重構前後對比

重構前

重構後

`section.hero .hero-intro h1`

`.hero__title`

`footer .social-media-link li img`

`.footer__social-media-icon`

`.header-nav li span`

`.header__nav-item`

**權重降低、可讀性提升** [valoremreply](https://www.valoremreply.com/resources/insights/guide/bem-methodology-a-step-by-step-guide-for-beginners/)

* * *

Part 3: 解決圖片超出 `<a>` 容器的問題
--------------------------

### 問題描述

在 footer 的社群媒體圖示區塊，`<img>` 被 `<a>` 標籤包住，但 `<a>` 容器的高度比 `<img>` 還小，導致圖片超出容器範圍。

### 原因分析

*   `<img>` 預設是 `inline` 元素，會受文字排版規則影響 [developer.mozilla](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_images/Replaced_element_properties)
    
*   Inline 元素的對齊基準是 `baseline`，下方會有預留空隙 [cnblogs](https://www.cnblogs.com/kidsitcn/p/5982784.html)
    
*   當 `<img>` 設定了 `width` 但 `<a>` 容器沒有明確高度時，容器可能無法正確被撐開
    

**視覺問題**：

    ┌─ <a> 容器 ──────┐
    │                  │
    │ ┌─ <img> ─────┐ │
    │ │              │ │
    │ │              │ │  ← 圖片底部超出
    │ └──────────────┘ │
    │   ↓ baseline 預留空隙
    └──────────────────┘
    

* * *

### 解決方案

    .footer__social-media-icon {
      width: 30px;
      display: block;  /* 將圖片改為 block 元素 */
    }
    

### 為什麼這樣有效？

1.  `display: block` 讓 `<img>` 從 inline 變成 block 元素 [developer.mozilla](https://developer.mozilla.org/zh-TW/docs/Glossary/Block/CSS)
    
2.  Block 元素會正常撐開父容器的高度
    
3.  不再受 baseline 和 line-height 等文字排版規則干擾 [cnblogs](https://www.cnblogs.com/kidsitcn/p/5982784.html)
    
4.  `<a>` 容器的尺寸會完全由 `<img>` 決定
    

**修正後**：

    ┌─ <a> 容器 ──────┐
    │ ┌─ <img> ─────┐ │
    │ │              │ │
    │ │              │ │
    │ └──────────────┘ │  ← 完美包住
    └──────────────────┘
    

* * *

### 適用場景

*   圖示按鈕（`<a>` 或 `<button>` 裡只有圖片）
    
*   純圖片連結，需要容器完全包住圖片
    
*   任何「父容器應該被子元素圖片完整撐開」的情境
    

* * *

常見問題 Q&A
--------

### Q1: 什麼時候一定要用三層結構？

**必須用三層**（骨架 > 容器 > 圖片）：

*   圖片是外部資源（網路圖片、使用者上傳）
    
*   圖片尺寸不確定
    
*   需要複雜定位（`absolute`, `transform`）
    
*   需要響應式調整
    

**可以簡化成兩層**：

*   圖片尺寸固定且可控（如 icon）
    
*   簡單的 block 排列
    
*   不需要複雜定位
    

* * *

### Q2: 為什麼 `.hero__img-box` 不用設定 `height`？

因為 `.hero__img` 設定了 `height: 100%`，形成：

*   `.hero__img-box` 的高度由內部 `.hero__img` 決定
    
*   `.hero__img` 的高度是 100%（相對於 `.hero__img-box`）
    

這看起來是「循環參照」，但實際上：

*   `.hero__img` 的原始高度（由圖片檔案決定）會先算出
    
*   `.hero__img-box` 被撐開到這個高度
    
*   `.hero__img` 再填滿 `.hero__img-box`
    

**結果**：圖片保持原始比例，同時填滿容器

* * *

### Q3: BEM 的 Element 可以嵌套嗎？

**不建議超過兩層** `__`： [hackmd](https://hackmd.io/@chupai/SkeNVToGr)

    /* ❌ 不要這樣 */
    .header__nav__item__link { ... }
    
    /* ✅ 應該這樣 */
    .header__nav-item { ... }
    .header__nav-link { ... }
    

BEM 建議扁平化命名，即使 HTML 結構有多層嵌套，CSS class 也應該保持簡單。

* * *

### Q4: 為什麼不能只用兩層（`.hero` + `.hero__img`）？

如果省略中間層：

    .hero__img {
      position: absolute;
      width: 45%;
      height: 100%;  /* = 450px，固定寬高比 */
      right: 0;
    }
    

**問題**：

*   `width: 45%` + `height: 100%` = 寬高比固定
    
*   如果圖片原始比例不是 45:100，會變形
    

加上 `.hero__img-box` 中間層：

*   圖片比例自適應
    
*   不會變形
    
*   版面骨架穩定
    

* * *

BEM 重構檢查清單
----------

### HTML 檢查

*   \[ \] 每個需要樣式的元素都有 class
    
*   \[ \] Block 名稱獨立且語意清楚
    
*   \[ \] Element 用 `__` 連接
    
*   \[ \] Modifier 用 `--` 連接
    
*   \[ \] 沒有混用 `-` 和 `__`
    

### CSS 檢查

*   \[ \] 沒有 tag selector（除了 reset）
    
*   \[ \] 沒有後代選擇器超過 2 層
    
*   \[ \] 每個 class 可以獨立解讀
    
*   \[ \] HTML 和 CSS 的 class 名稱完全對應
    
*   \[ \] 同一 Block 的所有 Element 都用相同前綴
    

* * *

重構成果總結
------

### 空間管理改善

*   ✅ 從依賴子元素推擠 → 父層明確定義空間
    
*   ✅ 從子元素可能超出 → 子元素相對父層尺寸
    
*   ✅ 從直接定位圖片 → 三層結構穩定版面
    

### 命名系統改善

*   ✅ 從 4 層選擇器 → 單一 class
    
*   ✅ 從混亂命名 → 系統化 BEM 命名
    
*   ✅ 從難以重用 → 模組化結構
    

### 可維護性提升

*   ✅ 版面穩定，不會因圖片載入而跳動
    
*   ✅ 命名可預測，看 class 就知道位置
    
*   ✅ 選擇器權重降低，避免樣式衝突 [valoremreply](https://www.valoremreply.com/resources/insights/guide/bem-methodology-a-step-by-step-guide-for-beginners/)
    

* * *

延伸學習資源
------

### 官方文件

*   [BEM 官方網站](https://getbem.com) [getbem](https://getbem.com)
    
*   [BEM Methodology](https://en.bem.info/methodology/css/) [en.bem](https://en.bem.info/methodology/css/)
    
*   [MDN: Replaced Elements](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_images/Replaced_element_properties) [developer.mozilla](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_images/Replaced_element_properties)
    
*   [MDN: CSS Block and Inline Layout](https://developer.mozilla.org/zh-TW/docs/Glossary/Block/CSS) [developer.mozilla](https://developer.mozilla.org/zh-TW/docs/Glossary/Block/CSS)
    

### 實務指南

*   [BEM for Beginners - Valorem Reply](https://www.valoremreply.com/resources/insights/guide/bem-methodology-a-step-by-step-guide-for-beginners/) [valoremreply](https://www.valoremreply.com/resources/insights/guide/bem-methodology-a-step-by-step-guide-for-beginners/)
    
*   [CSS Guidelines - Chris Pearce](https://github.com/chris-pearce/css-guidelines)
    

* * *

下一步行動
-----

1.  **複習三個原則**，找出你目前程式碼中還沒套用的地方
    
2.  **試著改寫一個區塊**，用三層結構重構
    
3.  **學習 Flexbox**，從 MDN 的 Flexbox Guide 開始
    
4.  **記錄「為什麼」**，不只是「怎麼做」
    

* * *

**筆記日期**：2026-02-12  
**學習情境**：ZERO TYPE Landing Page 完整重構  
**技術標籤**：#CSS #BoxModel #BEM #ReplacedElements #LayoutManagement  
**重構結果**：✅ 完整符合 BEM 規範，版面穩定可維護

---

*Originally published on [雞蛋糕的前端修煉屋](https://paragraph.com/@gcake/day-58-59)*
