🌐Vue 3 表格「表頭固定 + 左欄固定」完整實戰:兩列表頭(rowspan)蓋不住?用 CSS 變數、position: sticky、z-index 一次搞定!

 

內容

這篇是給「對 CSS 不熟,但需要把表格頭固定」的人看的超清楚指南。
你會學到如何在 Vue 3 中打造「多列表頭(兩列) + 左邊固定欄位 + 水平捲動」的表格,並解決最常見的兩個痛點:

  1. 第二列表頭把第一列擋住、或蓋不齊。

  2. 左側固定欄(如 SEQ_NO)在跨兩列表頭時高度不夠、蓋不住導致漏縫。



問題長什麼樣?

  • 表格表頭有兩列(第一列是大群組、第二列是子欄位)。

  • 表頭與左側第一欄要固定(position: sticky)。

  • 捲動時會發生:

    • 第二列表頭的 top 沒算好,導致重疊露出縫隙

    • 左側固定欄(常見是 SEQ_NO)設了 rowspan="2",但高度只吃到第一列,沒有蓋到第二列。

這些問題幾乎都源自 高度(height/top)與層級(z-index 沒設定好。



解法核心觀念(先理解,後面照貼就成功)

  1. 兩列表頭要分別 sticky:

    • 第一列:top: 0

    • 第二列:top: <第一列的高度>

  2. 左側固定欄位(跨兩列)要有「兩列加總的高度」 才能把兩列表頭「一次蓋住」。

  3. 層級順序(z-index 不能打架:

    • 左上角的「跨兩列」表頭要最高(它蓋兩列)。

    • 接著是第二列表頭,再來第一列表頭,再來內文儲存格。

  4. 表格建議用 border-collapse: separate; border-spacing: 0;
    這樣 position: sticky 在多瀏覽器會更穩。



最小可行範例(Template)

<table> <thead> <!-- 第一列表頭(群組列) --> <tr> <!-- 左上角:跨兩列 + 左欄固定 --> <th class="sticky-left sticky-span2" rowspan="2">SEQ_NO 序號</th> <!-- 其他群組欄位 --> <th rowspan="2">APS方案</th> <th rowspan="2">SITE</th> <!-- 群組標題(示例) --> <th colspan="2" class="group-tft">評估明細_TFT</th> <th colspan="2" class="group-lcm">評估明細_LCM</th> </tr> <!-- 第二列表頭(子欄位列) --> <tr> <th>退周數量</th> <th>退周周別</th> <th>退周數量</th> <th>退周周別</th> </tr> </thead> <tbody> <tr> <td class="sticky-left">1</td> <td>1</td> <td>NGB</td> <td>0</td> <td>W2541</td> <td>0</td> <td>W2541</td> </tr> <!-- ... --> </tbody> </table>


一次貼好就能動的 CSS(重點都有註解)

直接貼,然後只需要微調兩個變數:第一列高度第二列高度

/* 1) 兩列表頭高度(依你的實際畫面調整) */ :root { --hdr1: 56px; /* 第一列表頭實際高度 */ --hdr2: 40px; /* 第二列表頭實際高度 */ } /* 2) 表格建議設定:sticky 較穩 */ table { border-collapse: separate; border-spacing: 0; width: max-content; /* 讓寬度隨內容展開,利於水平捲動 */ font-size: 14px; } thead th { background: #ffd700; color: #000; white-space: nowrap; border: 1px solid #ddd; padding: 10px; text-align: center; } /* 3) 第一列表頭固定 */ thead tr:nth-of-type(1) th { position: sticky; top: 0; z-index: 11; /* 比內容高就好,後面還有更高的 */ height: var(--hdr1); /* 鎖定第一列高度,避免第二列壓上來 */ } /* 4) 第二列表頭固定:top 要等於「第一列高度」 */ thead tr:nth-of-type(2) th { position: sticky; top: var(--hdr1); z-index: 20; /* 比第一列高,因為它會蓋在第一列下緣 */ height: var(--hdr2); } /* 5) 左側固定欄(內文用) */ tbody .sticky-left { position: sticky; left: 0; background: #fff; /* 蓋住底層 */ z-index: 6; /* 比一般儲存格高就好 */ border-right: 1px solid #ddd; } /* 6) 左上角的「跨兩列」表頭(最關鍵!)*/ thead .sticky-left.sticky-span2, thead th.sticky-left[rowspan="2"] { position: sticky; left: 0; top: 0; height: calc(var(--hdr1) + var(--hdr2)); /* ⇐ 兩列加總高度 */ line-height: calc(var(--hdr1) + var(--hdr2)); /* 讓文字垂直置中(可選) */ z-index: 60; /* 最高,蓋住兩列 */ background: #ffd700; /* 保持金色底 */ border-right: 1px solid #ddd; } /* 7) 好看一點的群組欄色塊(非必需) */ .group-tft { background: #fff0ac; color: #000; } .group-lcm { background: #ffbb77; color: #000; }

小提醒:若你的樣式表中還有其他 thead .sticky-left { z-index: 20; } 之類的舊規則,請把上面第 6 點的規則放在最後(或加 !important),避免被覆蓋。



常見坑與排錯

  1. 第二列表頭還是擋住第一列?

    • 檢查 thead tr:nth-of-type(2) th { top: var(--hdr1) }--hdr1 是否正確。

    • 檢查有沒有其他 CSS 把 topz-index 改掉。

  2. 左上角(跨兩列)沒蓋住第二列?

    • 確認該格有 rowspan="2",並套用 .sticky-left.sticky-span2 或選擇器 thead th.sticky-left[rowspan="2"]

    • 把高度設為 calc(var(--hdr1) + var(--hdr2))z-index 調高(如 60)。

  3. sticky 完全不生效?

    • 父層不能 overflow: hidden(或需小心使用)。

    • table 請用 border-collapse: separate

    • 確認實際有「捲動容器」(通常是外層的 .table-container)。

  4. 固定欄位邊界變「鋸齒」或看不到邊線

    • 幫固定欄位右側加一條假邊線:border-right: 1px solid #ddd;



Vue 3 小補充:同步底部水平捲動條(可選)

大寬表格常會做「底部同步捲動條」。概念是兩個容器互相同步 scrollLeft

<div class="table-container" ref="tableContainer" @scroll="syncScrollTable"> <!-- 表格 --> </div> <!-- 底部捲動條 --> <div class="bottom-scroll" ref="bottomScroll" @scroll="syncScrollBottom"> <div class="dummy-scroll" ref="dummyScroll"></div> </div>
const tableContainer = ref(null) const bottomScroll = ref(null) const dummyScroll = ref(null) function syncScrollBottom() { if (!bottomScroll.value || !tableContainer.value) return tableContainer.value.scrollLeft = bottomScroll.value.scrollLeft } function syncScrollTable() { if (!bottomScroll.value || !tableContainer.value) return bottomScroll.value.scrollLeft = tableContainer.value.scrollLeft } function updateDummyScrollWidth() { // 設一個足夠寬的假內容,讓底部可以捲 const fullWidth = tableContainer.value?.scrollWidth ?? 0 if (dummyScroll.value) dummyScroll.value.style.width = fullWidth + 'px' }
.bottom-scroll { position: fixed; bottom: 0; left: 0; width: 100%; height: 20px; overflow-x: auto; overflow-y: hidden; background: #f5f5f5; z-index: 1000; } .dummy-scroll { height: 1px; }


最後的檢查清單(照著對)

  • 第一列表頭:top: 0; height: var(--hdr1); z-index: 11

  • 第二列表頭:top: var(--hdr1); height: var(--hdr2); z-index: 20

  • 左上角跨兩列:height: calc(var(--hdr1) + var(--hdr2)); z-index: 60

  • 左側固定欄:position: sticky; left: 0; background: #fff; z-index: 6

  • table { border-collapse: separate; border-spacing: 0; }

  • 需要時加 border-right 讓邊界清楚

  • 避免父層把 overflow 設得影響 sticky



結語

只要抓住三個點:高度top 位移z-index 層級,多列表頭 + 左欄固定其實一點都不難。把兩列表頭的高度用 CSS 變數控制,再針對左上角的跨兩列格子加總高度與最高層級,就能完美蓋住所有狀況。
有了這套,無論你要做供應鏈看板、報表、管理後台的大寬表格,都能維持 穩定可讀好維護

— 祝你一次就黏住不飄 🎯

留言

這個網誌中的熱門文章

🛠【ASP.NET Core + Oracle】解決 ORA-00904 "FALSE": 無效的 ID 錯誤與資料欄位動態插入顯示問題

🛠【實戰排除教學】從 VS Code 的 _logger 錯誤,到 PowerShell 找不到 npm/serve,再到 Oracle ORA-03135 連線中斷——一次搞懂!

🔎如何在 Oracle PL/SQL 儲存過程中為文字欄位加入換行符號(CHR(10))——以 Updlcmremark 為例