ตารางที่มีคอลัมน์เยอะ ๆ อย่างยอดขายรายเดือน 12 เดือน พอเปิดบนมือถือก็ต้องเลื่อนไปทางขวา เลื่อนไปสักพักเราก็ลืมแล้วว่าแถวนี้เป็นของสาขาไหน ใน Excel เราแก้ด้วย Freeze Panes บนเว็บเราทำแบบเดียวกันได้ด้วย CSS position: sticky โดยไม่ต้องเปลี่ยนตารางไปใช้ div หรือ layout แบบอื่น
ตัวอย่างในบทความนี้เขียนด้วยคลาสของ Tailwind CSS v4 และมีเวอร์ชัน CSS ธรรมดาไว้ท้ายบทความ
ใส่ wrapper ที่เลื่อนแนวนอนได้
เริ่มจากหุ้มตารางด้วย div ที่ใส่ overflow-x-auto กับ w-full ถ้าตารางกว้างกว่าจอ wrapper จะโชว์ scrollbar ให้เลื่อนซ้ายขวาเฉพาะในกรอบนี้ ส่วนอื่นของหน้าไม่เลื่อนตาม
<div class="w-full overflow-x-auto">
<table>
<!-- thead, tbody -->
</table>
</div>
ล็อกคอลัมน์แรกด้วย sticky left-0
ทีนี้ใส่ sticky left-0 ให้ช่องแรกของทุกแถว ช่องนั้นจะติดอยู่ที่ขอบซ้ายของ wrapper ตอนเราเลื่อนตาราง
<tr>
<th scope="row" class="sticky left-0 z-10 bg-white">ร้านสาขาสยาม</th>
<td>245,000</td>
<td>198,000</td>
<!-- คอลัมน์อื่น ๆ -->
</tr>
stickyกับleft-0ให้ช่องแรกของแต่ละแถวติดอยู่ที่ขอบซ้ายbg-whiteใส่สีพื้นให้ช่องนั้น ไม่อย่างนั้นตัวเลขของคอลัมน์ถัดไปจะเลื่อนมาซ้อนใต้ชื่อสาขาให้เห็น จะเปลี่ยนเป็นสีอื่นให้ตรงกับดีไซน์ของตารางก็ได้z-10ให้ช่องที่ sticky ลอยอยู่เหนือช่องอื่นที่เลื่อนผ่านไปข้างใต้
ช่องแรกเป็นชื่อของแถว เราจึงใช้ <th scope="row"> แทน <td> screen reader จะได้อ่านชื่อสาขาให้ทุกครั้งที่ผู้ใช้ไล่ตัวเลขในแถวนั้น
ไม่ต้องใส่ relative ให้ table นะ (บทความนี้เวอร์ชันแรกบอกว่าต้องใส่ ซึ่งไม่จำเป็น) element ที่เป็น sticky จะยึดกับกล่องที่ scroll ได้ที่อยู่ใกล้ที่สุด ซึ่งในที่นี้คือ wrapper ที่ใส่ overflow-x-auto ไว้
เลื่อนตารางไปทางขวา คอลัมน์แรกยังติดอยู่ที่ขอบซ้าย
ล็อกหัวตารางด้วย sticky top-0
ถ้าตารางยาวลงไปหลายแถวด้วย เราก็อยากให้หัวตารางติดอยู่ข้างบนเหมือนกัน ใส่ sticky top-0 ที่ th ในแถวหัวตาราง แล้วเปลี่ยน wrapper เป็น overflow-auto กับกำหนดความสูงสูงสุด เช่น max-h-[70vh]
<div class="max-h-[70vh] w-full overflow-auto">
<table>
<thead>
<tr>
<th class="sticky top-0 left-0 z-30 bg-gray-100">สาขา</th>
<th class="sticky top-0 z-20 bg-gray-100">ม.ค. 2569</th>
<th class="sticky top-0 z-20 bg-gray-100">ก.พ. 2569</th>
<!-- หัวคอลัมน์อื่น ๆ -->
</tr>
</thead>
<!-- tbody -->
</table>
</div>
ช่องมุมซ้ายบนต้องมี z-index สูงสุด
ช่อง "สาขา" ที่มุมซ้ายบนทั้ง sticky ซ้ายและ sticky บน ตอนเลื่อนทแยงจะมีทั้งหัวตารางและคอลัมน์แรกวิ่งผ่านใต้ช่องนี้ เราจึงไล่ z-index เป็นสามชั้น
| ช่อง | คลาส | ชั้น |
|---|---|---|
คอลัมน์แรกใน tbody |
sticky left-0 z-10 |
ล่างสุด |
| หัวตาราง | sticky top-0 z-20 |
ทับคอลัมน์แรกตอนเลื่อนลง |
| มุมซ้ายบน | sticky top-0 left-0 z-30 |
บนสุด |
ถ้าหน้าเว็บมี dropdown หรือ navbar ที่ fix ไว้ด้วย ให้ตั้ง z-index ของ element พวกนั้นให้สูงกว่า z-30 ไม่อย่างนั้นหัวตารางจะลอยทับ dropdown
เส้นขอบหายตอนเลื่อน เพราะ border-collapse
ตารางที่มีเส้นขอบมักเจอปัญหานี้ พอเลื่อนแล้วเส้นขอบของช่องที่ sticky หายไป หรือค้างอยู่ที่เดิมแทนที่จะเลื่อนตามช่อง สาเหตุคือ border-collapse: collapse ซึ่ง Tailwind ใส่ให้ทุก table ใน preflight อยู่แล้ว โหมดนี้รวมเส้นขอบของช่องที่ติดกันเป็นเส้นเดียว และเส้นนั้นไม่ได้เลื่อนไปกับช่องที่ sticky
แก้ได้ด้วยการเปลี่ยนตารางเป็น border-separate border-spacing-0 แล้วใส่เส้นขอบที่แต่ละช่องเองด้วย border-r border-b ช่องที่ติดกันจะไม่มีช่องว่างระหว่างกัน หน้าตาเหมือนเดิม แต่เส้นขอบจะเลื่อนไปพร้อมช่องของตัวเอง
ใน v4 สีเส้นขอบตั้งต้นเปลี่ยนจาก gray-200 เป็น currentColor (upgrade guide) เราจึงใส่ border-gray-200 กำกับไว้ด้วย ไม่อย่างนั้นเส้นจะเข้มเท่าสีตัวอักษร
โค้ดทั้งหมด
<div class="max-h-[70vh] w-full overflow-auto">
<table class="border-separate border-spacing-0">
<thead>
<tr>
<th class="sticky top-0 left-0 z-30 border-r border-b border-gray-200 bg-gray-100">สาขา</th>
<th class="sticky top-0 z-20 border-r border-b border-gray-200 bg-gray-100">ม.ค. 2569</th>
<!-- หัวคอลัมน์อื่น ๆ -->
</tr>
</thead>
<tbody>
<tr>
<th scope="row" class="sticky left-0 z-10 border-r border-b border-gray-200 bg-white">ร้านสาขาสยาม</th>
<td class="border-r border-b border-gray-200">245,000</td>
<!-- คอลัมน์อื่น ๆ -->
</tr>
</tbody>
</table>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>สาขา</th>
<th>ม.ค. 2569</th>
<!-- หัวคอลัมน์อื่น ๆ -->
</tr>
</thead>
<tbody>
<tr>
<th scope="row">ร้านสาขาสยาม</th>
<td>245,000</td>
<!-- คอลัมน์อื่น ๆ -->
</tr>
</tbody>
</table>
</div>
.table-wrap {
overflow: auto;
max-height: 70vh;
}
.table-wrap table {
border-collapse: separate;
border-spacing: 0;
}
.table-wrap th,
.table-wrap td {
border-right: 1px solid #e5e7eb;
border-bottom: 1px solid #e5e7eb;
background: #fff;
white-space: nowrap;
}
.table-wrap thead th {
position: sticky;
top: 0;
z-index: 20;
background: #f3f4f6;
}
.table-wrap tbody th {
position: sticky;
left: 0;
z-index: 10;
}
.table-wrap thead th:first-child {
left: 0;
z-index: 30;
}
เลื่อนตารางลงและไปทางขวาจนถึงเดือน ธ.ค. แล้ว แถวหัวตารางยังติดอยู่ข้างบน คอลัมน์ชื่อสาขายังติดอยู่ทางซ้าย และช่อง "สาขา" ที่มุมอยู่บนสุด




