Article · programming
EP20: CSS Flexbox เชิงลึก — สร้าง Navigation, Toolbar และ Layout ที่ยืดหยุ่น
HTML & CSS
เรียนรู้ CSS Flexbox ตั้งแต่แกนหลัก การจัดแนว การกระจายพื้นที่ การตัดบรรทัด การกำหนดขนาด และการสร้าง Navigation, Toolbar, Card และ Responsive Layout พร้อมตัวอย่าง HTML ที่ทดลองใช้งานได้ทันที
EP20: CSS Flexbox เชิงลึก — จัดวางองค์ประกอบให้ยืดหยุ่นและตอบสนองทุกหน้าจอ
เมื่อเว็บไซต์มีเมนูนำทาง แถบเครื่องมือ ปุ่มหลายปุ่ม การ์ดข้อมูล หรือส่วนหัวที่ต้องจัดองค์ประกอบให้อยู่ในแนวเดียวกัน การใช้ margin และการกำหนดตำแหน่งด้วยตัวเลขจำนวนมากอาจทำให้โค้ดดูแลรักษายาก CSS Flexbox ช่วยจัดวางองค์ประกอบในหนึ่งมิติได้อย่างยืดหยุ่น โดยสามารถควบคุมทิศทาง การจัดแนว ระยะห่าง และการแบ่งพื้นที่ระหว่างรายการได้อย่างเป็นระบบ บทเรียนนี้จะพาไปสร้างชิ้นส่วน UI ที่นำไปใช้ในเว็บไซต์จริงได้
🎯 เป้าหมายการเรียนรู้เมื่อจบบทเรียน คุณจะสามารถกำหนด flex container และ flex items เลือกใช้ justify-content, align-items, flex-wrap, gap, flex-grow, flex-shrink และ flex-basis รวมถึงออกแบบ Navigation และ Toolbar ที่ใช้งานได้บนทั้งจอคอมพิวเตอร์และมือถือ
1. Flexbox คืออะไร และเหมาะกับงานแบบไหน
Flexbox หรือ Flexible Box Layout เป็นระบบจัดวางของ CSS ที่ออกแบบมาสำหรับการจัดองค์ประกอบตามแกนหลักหนึ่งแกน เช่น แนวนอนหรือแนวตั้ง เหมาะกับ Navigation Bar, Toolbar, กลุ่มปุ่ม, แถวข้อมูล, ส่วนหัวของการ์ด และการจัดวางเนื้อหาที่ต้องปรับขนาดตามพื้นที่ที่มีอยู่ หากต้องสร้างโครงสร้างตารางหลายแถวหลายคอลัมน์ที่ซับซ้อน CSS Grid มักเหมาะกว่า แต่ทั้งสองระบบสามารถใช้ร่วมกันได้
| คุณสมบัติ | ความหมาย | ตัวอย่างการใช้งาน |
|---|---|---|
| display: flex | เปลี่ยนองค์ประกอบให้เป็น Flex Container | จัดเมนูให้อยู่ในแถวเดียวกัน |
| flex-direction | กำหนดทิศทางของแกนหลัก | row หรือ column |
| justify-content | จัดตำแหน่งตามแกนหลัก | กระจายเมนูหรือปุ่ม |
| align-items | จัดแนวตามแกนขวาง | จัดไอคอนกับข้อความให้อยู่กึ่งกลาง |
| gap | กำหนดช่องว่างระหว่างรายการ | เว้นระยะระหว่างปุ่มโดยไม่ใช้ margin แยกรายการ |
| flex-wrap | อนุญาตให้รายการขึ้นบรรทัดใหม่ | แสดงปุ่มหลายรายการบนหน้าจอเล็ก |
2. เข้าใจ Flex Container และ Flex Items
องค์ประกอบที่กำหนด display: flex จะเรียกว่า Flex Container ส่วนองค์ประกอบลูกโดยตรงจะเรียกว่า Flex Items ตัวอย่างเช่น เมื่อกำหนด display: flex ให้กับ nav รายการลิงก์ที่เป็นลูกโดยตรงจะถูกจัดเรียงตามทิศทางของ Flexbox ส่วนข้อความภายในลิงก์ยังสามารถใช้ CSS ปกติในการจัดรูปแบบได้
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ทดลอง Flexbox เบื้องต้น</title>
<style>
* { box-sizing: border-box; }
body { margin: 0; padding: 24px; font-family: Arial, sans-serif; background: #f1f5f9; color: #0f172a; }
h1 { font-size: 1.5rem; }
.flex-container { display: flex; gap: 12px; padding: 16px; background: #dbeafe; border: 2px dashed #2563eb; border-radius: 12px; }
.item { flex: 1; padding: 22px 12px; text-align: center; color: white; background: #2563eb; border-radius: 8px; font-weight: bold; }
.item:nth-child(2) { background: #0f766e; }
.item:nth-child(3) { background: #7c3aed; }
p { line-height: 1.7; }
</style>
</head>
<body>
<h1>Flex Container และ Flex Items</h1>
<p>กรอบเส้นประคือ Container ส่วนกล่องสีแต่ละกล่องคือ Item</p>
<div class="flex-container">
<div class="item">Item 1</div>
<div class="item">Item 2</div>
<div class="item">Item 3</div>
</div>
<p>ลองเปลี่ยน flex-direction จาก row เป็น column เพื่อจัดเรียงในแนวตั้ง</p>
</body>
</html>
3. flex-direction: เลือกทิศทางการจัดวาง
ค่าเริ่มต้นของ flex-direction คือ row ซึ่งจัดรายการตามแนวแถว ส่วน column จะจัดรายการตามแนวคอลัมน์ นอกจากนี้ยังมี row-reverse และ column-reverse แต่ควรระวังการใช้ค่าที่กลับลำดับ เพราะลำดับการอ่านด้วยคีย์บอร์ดและโปรแกรมอ่านหน้าจออาจไม่ตรงกับลำดับที่ผู้ใช้มองเห็น
| ค่า | ผลลัพธ์ | สถานการณ์ที่เหมาะสม |
|---|---|---|
| row | เรียงจากต้นแถวไปท้ายแถว | เมนูและ Toolbar |
| column | เรียงจากบนลงล่าง | ฟอร์มและกลุ่มข้อมูล |
| row-reverse | กลับลำดับในแนวแถว | ใช้เฉพาะกรณีที่ต้องการลำดับการแสดงผลกลับด้านจริง ๆ |
| column-reverse | กลับลำดับในแนวคอลัมน์ | ใช้ด้วยความระมัดระวังเมื่อเนื้อหามีลำดับความหมาย |
4. justify-content และ align-items ต่างกันอย่างไร
จำหลักสำคัญไว้ว่า justify-content ใช้จัดตำแหน่งตามแกนหลัก ส่วน align-items ใช้จัดแนวตามแกนขวาง โดยแกนทั้งสองจะเปลี่ยนไปตาม flex-direction หากใช้ row แกนหลักจะเป็นแนวนอนและแกนขวางเป็นแนวตั้ง แต่เมื่อใช้ column แกนหลักจะกลายเป็นแนวตั้งแทน
| คุณสมบัติ | ค่าที่ใช้บ่อย | ผลลัพธ์ |
|---|---|---|
| justify-content | flex-start | จัดชิดจุดเริ่มต้นของแกนหลัก |
| justify-content | center | จัดกึ่งกลางตามแกนหลัก |
| justify-content | space-between | กระจายรายการ โดยไม่มีช่องว่างส่วนเกินที่ขอบสองด้าน |
| justify-content | space-around | กระจายพื้นที่รอบรายการ |
| justify-content | space-evenly | กระจายช่องว่างให้เท่ากัน |
| align-items | center | จัดรายการให้อยู่กึ่งกลางแกนขวาง |
| align-items | stretch | ยืดรายการตามแกนขวางเมื่อขนาดที่กำหนดอนุญาต |
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ทดลองจัดแนว Flexbox</title>
<style>
* { box-sizing: border-box; }
body { margin: 0; padding: 24px; font-family: Arial, sans-serif; background: #f8fafc; }
.stage { display: flex; justify-content: space-between; align-items: center; gap: 12px; min-height: 180px; padding: 16px; background: #e0f2fe; border-radius: 12px; }
.box { display: grid; place-items: center; width: 76px; height: 56px; background: #0369a1; color: white; border-radius: 8px; font-weight: bold; }
.box:nth-child(2) { height: 92px; background: #0f766e; }
.box:nth-child(3) { height: 120px; background: #7c3aed; }
p { line-height: 1.7; }
</style>
</head>
<body>
<h2>จัดแนวตามแกนหลักและแกนขวาง</h2>
<p>กล่องถูกกระจายตามแนวนอนและจัดกึ่งกลางตามแนวตั้ง</p>
<div class="stage">
<div class="box">A</div>
<div class="box">B</div>
<div class="box">C</div>
</div>
</body>
</html>
⚠️ จุดที่มักทำให้สับสนอย่าจำว่า justify-content คือแนวนอนเสมอ เพราะเมื่อ flex-direction: column แกนหลักจะเป็นแนวตั้ง ให้พิจารณาทิศทางของแกนก่อนเลือกคุณสมบัติในการจัดแนว
5. gap และ flex-wrap: จัดระยะห่างพร้อมรองรับหน้าจอเล็ก
คุณสมบัติ gap กำหนดระยะห่างระหว่าง Flex Items โดยไม่เพิ่มช่องว่างที่ขอบนอกของ Container ส่วน flex-wrap: wrap อนุญาตให้รายการขึ้นบรรทัดใหม่เมื่อพื้นที่ไม่เพียงพอ การใช้สองคุณสมบัตินี้ร่วมกันเหมาะกับแถบตัวกรอง ปุ่มหมวดหมู่ และกลุ่มเครื่องมือที่ต้องทำงานบนหลายขนาดหน้าจอ
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Flex Wrap และ Gap</title>
<style>
* { box-sizing: border-box; }
body { margin: 0; padding: 24px; font-family: Arial, sans-serif; background: #f1f5f9; }
.filters { display: flex; flex-wrap: wrap; gap: 10px; padding: 16px; background: white; border: 1px solid #cbd5e1; border-radius: 14px; }
.filter { padding: 10px 14px; border: 0; border-radius: 999px; background: #dbeafe; color: #1d4ed8; font-weight: bold; }
.filter.active { background: #1d4ed8; color: white; }
</style>
</head>
<body>
<h2>ตัวกรองหมวดหมู่</h2>
<p>ลดความกว้างของหน้าต่างเพื่อดูว่าปุ่มขึ้นบรรทัดใหม่โดยอัตโนมัติ</p>
<div class="filters">
<button class="filter active">ทั้งหมด</button>
<button class="filter">บทเรียน</button>
<button class="filter">บทความ</button>
<button class="filter">ตัวอย่างโค้ด</button>
<button class="filter">แบบฝึกหัด</button>
<button class="filter">เครื่องมือ</button>
</div>
</body>
</html>
6. flex-grow, flex-shrink และ flex-basis
คุณสมบัติทั้งสามควบคุมการใช้พื้นที่ของแต่ละรายการ flex-grow กำหนดสัดส่วนการขยายเมื่อมีพื้นที่เหลือ flex-shrink กำหนดความสามารถในการหดตัวเมื่อพื้นที่ไม่พอ และ flex-basis กำหนดขนาดตั้งต้นก่อนคำนวณการแบ่งพื้นที่ ค่าที่ใช้บ่อยคือ flex: 1 ซึ่งช่วยให้รายการแบ่งพื้นที่ที่เหลือได้ใกล้เคียงกัน แต่ควรเข้าใจว่าค่านี้เป็น shorthand ที่รวมหลายคุณสมบัติเข้าด้วยกัน
| คุณสมบัติ | ตัวอย่าง | ความหมาย |
|---|---|---|
| flex-grow | flex-grow: 1 | อนุญาตให้ขยายเมื่อมีพื้นที่เหลือ |
| flex-shrink | flex-shrink: 0 | ไม่ให้หดจากกลไก flex-shrink |
| flex-basis | flex-basis: 220px | กำหนดขนาดตั้งต้นของรายการ |
| flex | flex: 1 1 220px | กำหนด grow, shrink และ basis ในบรรทัดเดียว |
ตัวอย่างเช่น การกำหนด flex: 1 1 220px ให้การ์ดแต่ละใบ หมายถึงการ์ดมีขนาดตั้งต้น 220px สามารถขยายเพื่อใช้พื้นที่เหลือและสามารถหดเมื่อพื้นที่ไม่เพียงพอ อย่างไรก็ตาม หากต้องการให้การ์ดขึ้นบรรทัดใหม่ ควรใช้ flex-wrap: wrap และพิจารณาความกว้างขั้นต่ำที่เหมาะสมร่วมด้วย
7. สร้าง Navigation Bar ด้วย Flexbox
Navigation Bar มักมีโลโก้ด้านหนึ่งและรายการเมนูอีกด้านหนึ่ง รูปแบบที่พบบ่อยคือใช้ display: flex; align-items: center; เพื่อจัดแนวในแนวขวาง และใช้ justify-content: space-between; เพื่อแยกพื้นที่ระหว่างแบรนด์กับเมนู สำหรับหน้าจอแคบสามารถให้เมนูตัดบรรทัดหรือเปลี่ยนการจัดวางด้วย Media Query ได้
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Navigation Bar ด้วย Flexbox</title>
<style>
* { box-sizing: border-box; }
body { margin: 0; font-family: Arial, sans-serif; background: #f8fafc; color: #0f172a; }
.navbar { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 16px; padding: 18px 24px; background: #0f172a; color: white; }
.brand { font-size: 1.2rem; font-weight: 800; white-space: nowrap; }
.menu { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.menu a { padding: 9px 12px; border-radius: 8px; color: #cbd5e1; text-decoration: none; }
.menu a:hover, .menu a:focus-visible { background: #334155; color: white; outline: none; }
main { max-width: 900px; margin: 32px auto; padding: 0 20px; }
.panel { padding: 24px; background: white; border: 1px solid #e2e8f0; border-radius: 16px; }
</style>
</head>
<body>
<header class="navbar">
<div class="brand">FlexStudio</div>
<nav class="menu" aria-label="เมนูหลัก">
<a href="#home">หน้าแรก</a>
<a href="#lessons">บทเรียน</a>
<a href="#projects">โปรเจกต์</a>
<a href="#contact">ติดต่อ</a>
</nav>
</header>
<main id="home">
<section class="panel">
<h1>Navigation ที่ยืดหยุ่น</h1>
<p>ปรับขนาดหน้าต่างเพื่อทดสอบการตัดบรรทัดของเมนู</p>
</section>
</main>
</body>
</html>
8. สร้าง Toolbar ที่มีช่องค้นหาและปุ่มด้านขวา
Toolbar มักประกอบด้วยช่องค้นหาและปุ่มดำเนินการ เมื่อใช้ Flexbox ให้ช่องค้นหาขยายกินพื้นที่ที่เหลือด้วย flex: 1 และกำหนด min-width: 0 เพื่อให้ช่องสามารถหดภายใน Flex Container ได้โดยไม่ดันปุ่มออกนอกกรอบ ส่วนปุ่มสามารถกำหนด flex-shrink: 0 หากต้องการรักษาขนาดไว้
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Toolbar ด้วย Flexbox</title>
<style>
* { box-sizing: border-box; }
body { margin: 0; padding: 24px; font-family: Arial, sans-serif; background: #f1f5f9; color: #0f172a; }
.toolbar { display: flex; align-items: center; gap: 10px; padding: 14px; background: white; border: 1px solid #cbd5e1; border-radius: 14px; box-shadow: 0 4px 12px rgba(15, 23, 42, .05); }
.search { flex: 1 1 180px; min-width: 0; padding: 11px 12px; border: 1px solid #cbd5e1; border-radius: 9px; font: inherit; }
.action { flex: 0 0 auto; padding: 11px 14px; border: 0; border-radius: 9px; background: #2563eb; color: white; font-weight: bold; cursor: pointer; }
.action.secondary { background: #e2e8f0; color: #0f172a; }
.result { margin-top: 16px; padding: 16px; background: white; border-radius: 12px; }
@media (max-width: 520px) { .toolbar { flex-wrap: wrap; } .search { flex-basis: 100%; } }
</style>
</head>
<body>
<h1>Toolbar ตัวอย่าง</h1>
<div class="toolbar">
<input class="search" id="search" type="search" placeholder="ค้นหารายการ..." aria-label="ค้นหารายการ">
<button class="action secondary" id="clear" type="button">ล้าง</button>
<button class="action" id="searchButton" type="button">ค้นหา</button>
</div>
<div class="result" id="result" aria-live="polite">กรอกคำค้นหาแล้วกดค้นหา</div>
<script>
const search = document.getElementById('search');
const result = document.getElementById('result');
document.getElementById('searchButton').addEventListener('click', () => {
const query = search.value.trim();
result.textContent = query ? 'ตัวอย่างผลการค้นหาสำหรับ: ' + query : 'กรุณากรอกคำค้นหาก่อน';
});
document.getElementById('clear').addEventListener('click', () => {
search.value = '';
result.textContent = 'ล้างคำค้นหาแล้ว';
search.focus();
});
</script>
</body>
</html>
9. จัดการกับข้อความยาวและปัญหา Overflow
หนึ่งในปัญหาที่พบได้บ่อยคือข้อความหรือ URL ยาวทำให้ Flex Item ไม่ยอมหดตามพื้นที่ที่มีอยู่ ค่าเริ่มต้น min-width: auto อาจรักษาขนาดขั้นต่ำตามเนื้อหาไว้ หากต้องการให้พื้นที่บางส่วนหดได้ ให้ลองกำหนด min-width: 0 ให้ Flex Item นั้น และใช้ overflow-wrap: anywhere กับข้อความที่อาจไม่มีจุดตัดบรรทัด เช่น รหัสหรือ URL ยาว
.toolbar {
display: flex;
align-items: center;
gap: 12px;
}
.toolbar__content {
flex: 1 1 auto;
min-width: 0;
}
.toolbar__content p {
overflow-wrap: anywhere;
}
.toolbar__action {
flex: 0 0 auto;
}
✅ วิธีคิดเมื่อต้องแก้ Overflowตรวจสอบก่อนว่า Flex Container มีความกว้างเท่าใด รายการใดควรยืด รายการใดควรรักษาขนาด และเนื้อหาภายในยอมตัดบรรทัดหรือไม่ จากนั้นจึงเลือกใช้ flex, min-width: 0, overflow-wrap หรือ flex-wrap ให้ตรงกับสาเหตุ แทนการแก้ด้วยค่าความกว้างแบบสุ่ม
10. สร้าง Card Row ที่ปรับจำนวนคอลัมน์ได้
Flexbox สามารถใช้สร้างแถวการ์ดที่มีความกว้างยืดหยุ่นได้ โดยกำหนด flex-wrap: wrap ให้ Container และกำหนด flex: 1 1 220px ให้การ์ดแต่ละใบ การ์ดจะมีขนาดตั้งต้นประมาณ 220px และสามารถแบ่งบรรทัดเมื่อพื้นที่ไม่พอได้ ทั้งนี้ขนาดจริงขึ้นอยู่กับความกว้างของ Container จำนวนรายการ และค่า gap
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Responsive Cards ด้วย Flexbox</title>
<style>
* { box-sizing: border-box; }
body { margin: 0; padding: 24px; font-family: Arial, sans-serif; background: #f8fafc; color: #0f172a; }
.cards { display: flex; flex-wrap: wrap; gap: 16px; }
.card { flex: 1 1 220px; min-width: 0; padding: 20px; background: white; border: 1px solid #e2e8f0; border-radius: 14px; box-shadow: 0 5px 16px rgba(15, 23, 42, .06); }
.card h2 { margin-top: 0; font-size: 1.15rem; }
.card p { line-height: 1.65; color: #475569; }
.tag { display: inline-block; padding: 5px 9px; background: #dbeafe; color: #1d4ed8; border-radius: 999px; font-size: .8rem; }
</style>
</head>
<body>
<h1>การ์ดที่ปรับตามพื้นที่</h1>
<div class="cards">
<article class="card"><span class="tag">HTML</span><h2>โครงสร้าง</h2><p>สร้างองค์ประกอบและจัดลำดับเนื้อหาให้มีความหมาย</p></article>
<article class="card"><span class="tag">CSS</span><h2>การออกแบบ</h2><p>ควบคุมสี ระยะห่าง และการจัดวางให้เหมาะกับอุปกรณ์</p></article>
<article class="card"><span class="tag">JavaScript</span><h2>ปฏิสัมพันธ์</h2><p>เพิ่มพฤติกรรมให้ปุ่ม ตัวกรอง และส่วนประกอบของหน้าเว็บ</p></article>
</div>
</body>
</html>
11. Responsive Flexbox ด้วย Media Query
การออกแบบ Responsive ไม่ได้หมายถึงการกำหนดจุดตัดหน้าจอจำนวนมาก แต่หมายถึงการเลือกพฤติกรรมที่เหมาะกับพื้นที่จริง ตัวอย่างเช่น Navigation อาจแสดงโลโก้และเมนูในแถวเดียวบนหน้าจอกว้าง แต่เปลี่ยนเป็นการจัดวางแนวตั้งเมื่อหน้าจอแคบ ส่วน Toolbar สามารถปล่อยให้ขึ้นบรรทัดใหม่ได้เมื่อช่องค้นหาและปุ่มเริ่มเบียดกัน
| เงื่อนไข | พฤติกรรมที่แนะนำ | เหตุผล |
|---|---|---|
| พื้นที่กว้าง | จัด Navigation เป็นแถวเดียว | ใช้พื้นที่แนวนอนได้เต็มที่ |
| พื้นที่ปานกลาง | อนุญาตให้เมนูตัดบรรทัด | ป้องกันรายการชนกัน |
| พื้นที่แคบ | จัด Toolbar เป็นหลายแถว | รักษาขนาดปุ่มและช่องกรอกให้ใช้งานง่าย |
12. ตัวอย่างโปรเจกต์เต็มหน้า: FlexStudio Responsive Workspace
ตัวอย่างสุดท้ายรวบรวมแนวคิดในบทเรียนไว้ในหน้าเดียว ประกอบด้วย Navigation, แถบเครื่องมือค้นหา, ปุ่มเปลี่ยนมุมมอง, การ์ดสรุปข้อมูล และรายการงาน ตัวอย่างนี้ใช้ Flexbox สำหรับการจัดวางองค์ประกอบหลักและใช้ Media Query เพื่อปรับประสบการณ์บนหน้าจอแคบ คุณสามารถคัดลอกโค้ดทั้งหมดไปบันทึกเป็น index.html แล้วเปิดด้วยเบราว์เซอร์ได้ทันที
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FlexStudio Workspace</title>
<style>
:root {
--bg: #f1f5f9;
--surface: #ffffff;
--text: #0f172a;
--muted: #64748b;
--primary: #2563eb;
--border: #e2e8f0;
--radius: 16px;
}
* { box-sizing: border-box; }
body { margin: 0; font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: var(--bg); color: var(--text); }
button, input { font: inherit; }
button { cursor: pointer; }
.app-header { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 16px; padding: 16px 24px; color: white; background: #0f172a; }
.brand { font-size: 1.15rem; font-weight: 800; letter-spacing: .02em; }
.header-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; }
.header-link { padding: 8px 11px; color: #e2e8f0; text-decoration: none; border-radius: 8px; }
.header-link:hover, .header-link:focus-visible { background: #334155; outline: none; }
.button { padding: 10px 14px; border: 0; border-radius: 10px; background: var(--primary); color: white; font-weight: 700; }
.button.secondary { background: #e2e8f0; color: #0f172a; }
.workspace { max-width: 1180px; margin: 0 auto; padding: 28px 20px 44px; }
.page-heading { display: flex; align-items: flex-end; justify-content: space-between; flex-wrap: wrap; gap: 16px; margin-bottom: 22px; }
.page-heading h1 { margin: 0 0 6px; font-size: clamp(1.5rem, 3vw, 2rem); }
.page-heading p { margin: 0; color: var(--muted); line-height: 1.6; }
.toolbar { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; margin-bottom: 20px; padding: 14px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); }
.search { flex: 1 1 220px; min-width: 0; padding: 11px 13px; border: 1px solid #cbd5e1; border-radius: 10px; background: white; }
.filter-label { color: var(--muted); font-size: .9rem; }
.stats { display: flex; flex-wrap: wrap; gap: 16px; margin-bottom: 22px; }
.stat { flex: 1 1 190px; min-width: 0; padding: 20px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); }
.stat-label { margin: 0 0 10px; color: var(--muted); font-size: .9rem; }
.stat-value { margin: 0; font-size: 1.8rem; font-weight: 800; }
.stat-note { margin: 8px 0 0; color: #0f766e; font-size: .85rem; }
.content-row { display: flex; align-items: stretch; flex-wrap: wrap; gap: 18px; }
.panel { flex: 1 1 320px; min-width: 0; padding: 22px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); }
.panel h2 { margin: 0 0 8px; font-size: 1.15rem; }
.panel-description { margin: 0 0 20px; color: var(--muted); font-size: .9rem; line-height: 1.6; }
.bars { display: flex; align-items: flex-end; gap: 12px; height: 180px; padding: 14px 8px 0; border-bottom: 1px solid var(--border); }
.bar-column { display: flex; flex: 1 1 0; flex-direction: column; align-items: center; justify-content: flex-end; gap: 8px; height: 100%; min-width: 0; }
.bar { width: min(100%, 42px); min-height: 8px; background: #93c5fd; border-radius: 8px 8px 2px 2px; }
.bar-column:nth-child(2n) .bar { background: #2563eb; }
.bar-label { color: var(--muted); font-size: .78rem; }
.task-list { display: flex; flex-direction: column; gap: 12px; margin: 0; padding: 0; list-style: none; }
.task { display: flex; align-items: flex-start; gap: 12px; padding: 12px; background: #f8fafc; border: 1px solid var(--border); border-radius: 12px; }
.task-marker { flex: 0 0 10px; width: 10px; height: 10px; margin-top: 6px; background: var(--primary); border-radius: 50%; }
.task-content { flex: 1 1 auto; min-width: 0; }
.task-title { margin: 0 0 4px; font-weight: 700; overflow-wrap: anywhere; }
.task-meta { margin: 0; color: var(--muted); font-size: .85rem; line-height: 1.5; }
.task-status { flex: 0 0 auto; padding: 5px 8px; color: #166534; background: #dcfce7; border-radius: 999px; font-size: .75rem; }
.empty { display: none; padding: 14px; color: var(--muted); text-align: center; }
.compact .stat { padding: 12px; }
.compact .panel { padding: 14px; }
.compact .workspace { padding-top: 16px; }
.notice { min-height: 24px; margin: 14px 0 0; color: var(--muted); font-size: .9rem; }
@media (max-width: 600px) {
.app-header { padding: 14px 16px; }
.workspace { padding: 20px 14px 32px; }
.header-actions { width: 100%; }
.header-link { padding-left: 0; }
.page-heading { align-items: flex-start; }
.toolbar { align-items: stretch; }
.search { flex-basis: 100%; }
.toolbar .button { flex: 1 1 auto; }
.task { flex-wrap: wrap; }
.task-status { margin-left: 22px; }
}
</style>
</head>
<body>
<header class="app-header">
<div class="brand">FlexStudio</div>
<nav class="header-actions" aria-label="เมนูหลัก">
<a class="header-link" href="#overview">ภาพรวม</a>
<a class="header-link" href="#analytics">สถิติ</a>
<a class="header-link" href="#tasks">งานของฉัน</a>
<button class="button secondary" id="compactToggle" type="button" aria-pressed="false">โหมดกะทัดรัด</button>
</nav>
</header>
<main class="workspace" id="overview">
<section class="page-heading">
<div>
<h1>พื้นที่ทำงานของคุณ</h1>
<p>ตัวอย่าง Dashboard ที่ใช้ Flexbox จัดวางและปรับตามพื้นที่หน้าจอ</p>
</div>
<button class="button" id="addTask" type="button">+ เพิ่มงานตัวอย่าง</button>
</section>
<section class="toolbar" aria-label="เครื่องมือค้นหา">
<label class="filter-label" for="taskSearch">ค้นหางาน</label>
<input class="search" id="taskSearch" type="search" placeholder="พิมพ์ชื่อโปรเจกต์หรือรายละเอียด...">
<button class="button secondary" id="clearSearch" type="button">ล้าง</button>
</section>
<section class="stats" aria-label="สรุปข้อมูล">
<article class="stat"><p class="stat-label">โปรเจกต์ทั้งหมด</p><p class="stat-value">12</p><p class="stat-note">กำลังดำเนินการ 4 โปรเจกต์</p></article>
<article class="stat"><p class="stat-label">งานที่เสร็จแล้ว</p><p class="stat-value">28</p><p class="stat-note">อัปเดตจากข้อมูลตัวอย่าง</p></article>
<article class="stat"><p class="stat-label">งานที่กำลังทำ</p><p class="stat-value" id="taskCount">3</p><p class="stat-note">ค้นหาและกรองรายการได้</p></article>
</section>
<section class="content-row">
<article class="panel" id="analytics">
<h2>กิจกรรมรายสัปดาห์</h2>
<p class="panel-description">กราฟแท่งตัวอย่างที่สร้างด้วย CSS ไม่ต้องติดตั้งไลบรารีเพิ่มเติม</p>
<div class="bars" role="img" aria-label="กราฟกิจกรรมวันจันทร์ถึงวันอาทิตย์">
<div class="bar-column"><div class="bar" style="height: 42%"></div><span class="bar-label">จ.</span></div>
<div class="bar-column"><div class="bar" style="height: 68%"></div><span class="bar-label">อ.</span></div>
<div class="bar-column"><div class="bar" style="height: 54%"></div><span class="bar-label">พ.</span></div>
<div class="bar-column"><div class="bar" style="height: 82%"></div><span class="bar-label">พฤ.</span></div>
<div class="bar-column"><div class="bar" style="height: 61%"></div><span class="bar-label">ศ.</span></div>
<div class="bar-column"><div class="bar" style="height: 94%"></div><span class="bar-label">ส.</span></div>
<div class="bar-column"><div class="bar" style="height: 47%"></div><span class="bar-label">อา.</span></div>
</div>
</article>
<article class="panel" id="tasks">
<h2>รายการงานล่าสุด</h2>
<p class="panel-description">ลองค้นหาด้วยคำว่า HTML, CSS หรือ Dashboard</p>
<ul class="task-list" id="taskList">
<li class="task" data-search="ออกแบบหน้า HTML โครงสร้างหน้าเว็บ"><span class="task-marker"></span><div class="task-content"><p class="task-title">ออกแบบหน้า HTML</p><p class="task-meta">โครงสร้างหน้าเว็บ · อัปเดตวันนี้</p></div><span class="task-status">กำลังทำ</span></li>
<li class="task" data-search="ปรับแต่ง CSS Responsive Design"><span class="task-marker"></span><div class="task-content"><p class="task-title">ปรับแต่ง CSS</p><p class="task-meta">Responsive Design · อัปเดตเมื่อวาน</p></div><span class="task-status">กำลังทำ</span></li>
<li class="task" data-search="ทดสอบ Dashboard Flexbox Layout"><span class="task-marker"></span><div class="task-content"><p class="task-title">ทดสอบ Dashboard</p><p class="task-meta">Flexbox Layout · อัปเดตล่าสุด</p></div><span class="task-status">กำลังทำ</span></li>
</ul>
<p class="empty" id="emptyState">ไม่พบงานที่ตรงกับคำค้นหา</p>
<p class="notice" id="notice" aria-live="polite">พร้อมใช้งาน — ทดลองค้นหา เพิ่มงาน หรือเปลี่ยนโหมดการแสดงผล</p>
</article>
</section>
</main>
<script>
const body = document.body;
const toggle = document.getElementById('compactToggle');
const search = document.getElementById('taskSearch');
const taskList = document.getElementById('taskList');
const emptyState = document.getElementById('emptyState');
const taskCount = document.getElementById('taskCount');
const notice = document.getElementById('notice');
let extraTaskNumber = 0;
toggle.addEventListener('click', () => {
const enabled = body.classList.toggle('compact');
toggle.setAttribute('aria-pressed', String(enabled));
toggle.textContent = enabled ? 'ปิดโหมดกะทัดรัด' : 'โหมดกะทัดรัด';
notice.textContent = enabled ? 'เปิดโหมดกะทัดรัดแล้ว' : 'กลับสู่การแสดงผลปกติแล้ว';
});
function filterTasks() {
const query = search.value.trim().toLocaleLowerCase('th');
const tasks = Array.from(taskList.querySelectorAll('.task'));
let visible = 0;
tasks.forEach((task) => {
const content = (task.dataset.search || task.textContent).toLocaleLowerCase('th');
const matches = content.includes(query);
task.hidden = !matches;
if (matches) visible += 1;
});
emptyState.style.display = visible === 0 ? 'block' : 'none';
taskCount.textContent = String(visible);
notice.textContent = query ? 'พบงาน ' + visible + ' รายการจากคำค้นหา' : 'แสดงงานทั้งหมด ' + visible + ' รายการ';
}
search.addEventListener('input', filterTasks);
document.getElementById('clearSearch').addEventListener('click', () => {
search.value = '';
filterTasks();
search.focus();
});
document.getElementById('addTask').addEventListener('click', () => {
extraTaskNumber += 1;
const item = document.createElement('li');
item.className = 'task';
item.dataset.search = 'งานทดลอง ' + extraTaskNumber + ' flexbox responsive';
const marker = document.createElement('span');
marker.className = 'task-marker';
const content = document.createElement('div');
content.className = 'task-content';
const title = document.createElement('p');
title.className = 'task-title';
title.textContent = 'งานทดลอง ' + extraTaskNumber;
const meta = document.createElement('p');
meta.className = 'task-meta';
meta.textContent = 'Flexbox Responsive · เพิ่มเมื่อสักครู่';
const status = document.createElement('span');
status.className = 'task-status';
status.textContent = 'งานใหม่';
content.append(title, meta);
item.append(marker, content, status);
taskList.prepend(item);
filterTasks();
notice.textContent = 'เพิ่มงานทดลอง ' + extraTaskNumber + ' แล้ว';
});
</script>
</body>
</html>
13. แนวทางตรวจสอบและแก้ปัญหา Flexbox
| อาการ | สาเหตุที่เป็นไปได้ | แนวทางแก้ไข |
|---|---|---|
| รายการไม่อยู่กึ่งกลาง | เลือกแกนหรือคุณสมบัติไม่ตรงกับ flex-direction | ตรวจสอบแกนหลักและแกนขวางก่อนใช้ justify-content หรือ align-items |
| รายการล้นออกนอกกรอบ | เนื้อหามีขนาดขั้นต่ำสูงหรือไม่ยอมตัดบรรทัด | ตรวจสอบ min-width: 0, overflow-wrap และ flex-wrap |
| รายการเบียดกัน | ไม่มี gap หรือพื้นที่ไม่พอ | เพิ่ม gap และอนุญาตให้ขึ้นบรรทัดใหม่ |
| ปุ่มถูกบีบจนอ่านยาก | รายการหดตัวมากเกินไป | พิจารณา flex-shrink: 0 หรือเปลี่ยนการจัดวางเมื่อจอแคบ |
| การ์ดมีความกว้างไม่สม่ำเสมอ | ค่า basis และเนื้อหาของแต่ละการ์ดแตกต่างกัน | กำหนด flex-basis และ min-width ให้เหมาะสม |
14. แบบฝึกหัดท้ายบท
ลองทำแบบฝึกหัดต่อไปนี้โดยแก้โค้ดตัวอย่างด้วยตนเอง แล้วทดสอบทั้งบนหน้าจอกว้างและหน้าจอแคบ
แบบฝึกหัดที่ 1: จัดเมนูให้อยู่กึ่งกลาง
สร้าง div ที่มีลิงก์อย่างน้อย 4 รายการ กำหนดให้ลิงก์เรียงเป็นแถว อยู่กึ่งกลางตามแกนหลัก และมีช่องว่างระหว่างรายการเท่ากัน โดยใช้ display: flex, justify-content และ gap
แบบฝึกหัดที่ 2: ทำการ์ดให้ขึ้นบรรทัดใหม่
สร้างการ์ด 5 ใบที่มีข้อความแตกต่างกัน กำหนด flex-wrap: wrap และ flex: 1 1 200px จากนั้นปรับขนาดหน้าต่างและสังเกตจำนวนการ์ดในแต่ละแถว
แบบฝึกหัดที่ 3: แก้ปัญหา Toolbar ล้น
สร้าง Toolbar ที่มีช่องค้นหาและปุ่ม 3 ปุ่ม ทดสอบด้วยข้อความค้นหาที่ยาวมาก จากนั้นแก้ปัญหาด้วย min-width: 0, flex-wrap และการกำหนด flex-shrink ให้เหมาะสม
แบบฝึกหัดที่ 4: เพิ่มโหมดมือถือ
นำ Navigation Bar จากบทเรียนไปเพิ่ม Media Query โดยเมื่อหน้าจอกว้างไม่เกิน 600px ให้รายการเมนูมีพื้นที่เต็มแถวและสามารถตัดบรรทัดได้โดยไม่เกิดการซ้อนทับ
🏆 เกณฑ์ตรวจงานด้วยตนเองงานถือว่าผ่านเมื่อไม่มีองค์ประกอบทับกันหรือหลุดออกนอกหน้าจอ ช่องว่างมีความสม่ำเสมอ ลำดับการอ่านยังสมเหตุสมผล ปุ่มและลิงก์เข้าถึงได้ด้วยคีย์บอร์ด และหน้าเว็บยังใช้งานได้เมื่อปรับความกว้างหน้าต่าง
15. คำถามที่พบบ่อย
Flexbox ต่างจาก CSS Grid อย่างไร?
Flexbox เหมาะกับการจัดวางตามแกนหลักหนึ่งแกน เช่น แถวของเมนูหรือคอลัมน์ของข้อมูล ส่วน CSS Grid เหมาะกับการกำหนดแถวและคอลัมน์ของโครงสร้างสองมิติ ทั้งสองระบบสามารถใช้ร่วมกันได้ตามความเหมาะสม
ควรใช้ margin หรือ gap สำหรับเว้นระยะห่าง?
เมื่อเป็นช่องว่างระหว่างรายการใน Flex Container การใช้ gap มักทำให้โค้ดกระชับและจัดการง่ายกว่า ส่วน margin ยังมีประโยชน์สำหรับระยะห่างเฉพาะองค์ประกอบหรือบริบทที่ไม่ได้เป็นช่องว่างระหว่างรายการใน Container
ทำไม Flex Item ถึงไม่ยอมหด?
เนื้อหาภายในอาจกำหนดขนาดขั้นต่ำโดยปริยาย ทำให้รายการไม่สามารถหดตามพื้นที่ได้ ลองตรวจสอบ min-width: 0, การตัดบรรทัด และค่า flex-shrink รวมถึงความกว้างของ Container
flex: 1 หมายถึงอะไร?
เป็น shorthand สำหรับกำหนด flex-grow, flex-shrink และ flex-basis ตามรูปแบบที่เบราว์เซอร์ใช้กับค่าที่ระบุ โดยทั่วไปช่วยให้รายการแบ่งพื้นที่ที่เหลือได้ แต่หากต้องการควบคุมขนาดตั้งต้นอย่างชัดเจน สามารถระบุทั้งสามค่า เช่น flex: 1 1 220px
จำเป็นต้องใช้ JavaScript เพื่อทำ Responsive Flexbox หรือไม่?
ไม่จำเป็น การจัดวางที่ตอบสนองต่อขนาดหน้าจอสามารถทำได้ด้วย Flexbox และ Media Query ส่วน JavaScript ควรใช้เมื่อจำเป็นต้องมีพฤติกรรมโต้ตอบ เช่น ค้นหารายการ เพิ่มข้อมูล หรือสลับโหมดการแสดงผล
Layout ที่ดีไม่ได้เกิดจากการบังคับทุกองค์ประกอบให้อยู่ในตำแหน่งตายตัว แต่เกิดจากการกำหนดกติกาที่ทำให้องค์ประกอบปรับตัวได้อย่างมีเหตุผล
สรุปบทเรียน
CSS Flexbox เป็นเครื่องมือสำคัญสำหรับการจัดวาง UI ที่ยืดหยุ่น หลักการที่ควรจำคือกำหนด display: flex ให้ Container เลือก flex-direction ให้ตรงกับทิศทางที่ต้องการ ใช้ justify-content สำหรับแกนหลักและ align-items สำหรับแกนขวาง ใช้ gap เพื่อเว้นระยะ ใช้ flex-wrap เมื่อรายการควรขึ้นบรรทัดใหม่ และตรวจสอบ flex-grow, flex-shrink, flex-basis รวมถึง min-width: 0 เมื่อเกิดปัญหาพื้นที่ล้น เมื่อนำหลักการเหล่านี้ไปใช้ร่วมกับ Media Query คุณจะสร้าง Navigation, Toolbar, Card Layout และ Dashboard ที่ดูแลรักษาง่ายและรองรับหลายขนาดหน้าจอได้ดีขึ้น
กลับไปทบทวนบทเรียน HTML และ CSS
อ่านจนถึงระยะ 500px ก่อนท้ายหน้าและอยู่ในหน้านี้อย่างน้อย 5 นาที เพื่อรับ EXP วันละครั้ง
⏳ กำลังจับเวลาอ่าน...