Buổi 9: Thiết kế bố cục (2): Flexbox, Grid và thiết kế đáp ứng
Hãy tưởng tượng bạn đang dựng trang chủ cho một cửa hàng sách trực tuyến. Trên máy tính, trang có thanh điều hướng nằm ngang, ba cột (danh mục bên trái, danh sách sách ở giữa, quảng cáo bên phải) và chân trang rộng hết bề ngang. Nhưng khi khách mở trang bằng điện thoại, ba cột đó phải xếp chồng thành một cột, thanh điều hướng thu thành một nút "☰", ảnh bìa sách co lại vừa màn hình. Nếu chỉ dùng float và position như ở buổi 8, bạn sẽ phải tính toán từng pixel và gần như chắc chắn gặp lỗi. Hai kỹ thuật bố cục hiện đại là Flexbox (bố cục linh hoạt) và Grid (bố cục lưới) sinh ra để giải quyết đúng bài toán này, còn thiết kế đáp ứng (responsive design) với viewport và @media là cách làm cho cùng một mã nguồn đẹp trên mọi thiết bị. Kết thúc buổi, bạn sẽ tự dựng được bố cục header – nav – content – footer có thể co giãn từ máy tính xuống điện thoại.
Mục tiêu buổi học
- Sau buổi này bạn có thể tạo flex container bằng
display: flexvà điều khiển hướng, xuống dòng, căn chỉnh các phần tử con bằngflex-direction,flex-wrap,flex-flow,justify-content,align-items,align-content. - Sau buổi này bạn có thể điều khiển từng flex item bằng
order,flex-grow,flex-shrink,flex-basis,flexvàalign-self, đồng thời giải thích được cách Flexbox chia không gian thừa hoặc thiếu. - Sau buổi này bạn có thể dựng lưới (grid) bằng
grid-template-columns,grid-template-rows,grid-template-areas,gap, đơn vịfr, hàmrepeat()vàminmax(), và đặt phần tử con vào đúng ô bằnggrid-column,grid-row,grid-area. - Sau buổi này bạn có thể thiết kế bố cục chung của một Website (header, nav, content 3 cột, footer) bằng Flexbox.
- Sau buổi này bạn có thể khai báo viewport, viết media queries, áp dụng nguyên tắc Mobile first và làm cho ảnh, thanh điều hướng, cỡ chữ đáp ứng theo kích thước màn hình.
Bản đồ buổi học
| Phần | Nội dung | Thời lượng gợi ý |
|---|---|---|
| 1. Bố cục linh hoạt (Flexbox) | Khái niệm, thuộc tính của container và của item | 40 phút (lý thuyết 15, thực hành 25) |
| 2. Bố cục lưới (Grid) | Container, fr, repeat(), minmax(), grid item, vùng đặt tên |
40 phút (lý thuyết 15, thực hành 25) |
| 3. Thiết kế bố cục cho Web | Bố cục chung, header, nav, content, footer | 25 phút (lý thuyết 10, thực hành 15) |
| 4. Thiết kế đáp ứng | Viewport, media queries, Mobile first, ảnh, menu, cỡ chữ | 40 phút (lý thuyết 15, thực hành 25) |
| Thực hành theo giáo trình | Bài 4.1, 4.2, 4.3, 4.4 | 25 phút |
| Ôn tập | Câu hỏi ôn tập, sai lầm thường gặp, tóm tắt | 10 phút |
| Tổng | 180 phút (thực hành khoảng 50%) |
1. Bố cục linh hoạt (Flexbox)
Giáo trình: Chương 4 — mục "Bố cục linh hoạt", gồm ba mục nhỏ: "Khái niệm về bố cục linh hoạt (Flex)", "Phần tử chứa (Flex container)" và "Phần tử con (Flex item)".
1.1. Khái niệm về bố cục linh hoạt (Flex)
Flex là một trong những giá trị có thể đặt cho thuộc tính display. Giá trị flex giúp tạo ra kỹ thuật thiết kế bố cục hộp linh hoạt, gọi là Flexbox. Cốt lõi của Flexbox là hai vai trò: container (phần tử cha, hộp chứa) và item (các phần tử con bên trong). Bạn thiết lập cấu hình cho container để quyết định cách sắp xếp tổng thể, và thiết lập cấu hình cho từng item để quyết định mỗi phần tử con chiếm bao nhiêu chỗ.
Điều khác biệt lớn nhất so với block và inline là: một flex container tự lo việc xếp các con thành hàng hoặc cột, tự chia không gian thừa, tự co lại khi thiếu chỗ. Bạn không còn phải dùng float, không cần clear, không phải tính tay độ rộng từng phần tử.
Ví dụ đầu tiên trong giáo trình: tạo một flex container chứa năm item. Hãy bấm chạy và quan sát: năm ô xếp trên một hàng thay vì chồng lên nhau như các thẻ div bình thường. Phần mã in ra bảng console cho biết giá trị display của container và của một item để bạn thấy chỉ có container được khai báo flex.
<style>
body { font-family: Arial; background-color: #f1f1f1; }
h1 { color: #1abc9c; text-align: center; margin-top: 10px; font-size: 22px; }
.container { display: flex; background-color: #1abc9c; }
.item { background-color: white; margin: 5px; padding: 20px; }
</style>
<div>
<h1>Flex container</h1>
<div class="container" id="ct">
<div class="item">Flex item</div>
<div class="item">Flex item</div>
<div class="item">Flex item</div>
<div class="item">Flex item</div>
<div class="item">Flex item</div>
</div>
</div>
<script>
const ct = document.getElementById("ct");
console.log("display của container:", getComputedStyle(ct).display);
console.log("display của item đầu tiên:", getComputedStyle(ct.firstElementChild).display);
console.log("Số item trong container:", ct.children.length);
</script>Lưu ý: Chỉ các con trực tiếp của flex container mới trở thành flex item. Cháu, chắt (phần tử lồng sâu hơn) vẫn hiển thị bình thường. Muốn chúng cũng là flex item của một container khác, bạn phải đặt
display: flexcho chính phần tử cha của chúng (lồng flex).
Khi dùng Flexbox, giáo trình yêu cầu bạn nắm rõ các thuật ngữ sau:
- Các trục (axis): main axis (trục chính) và cross axis (trục phụ, vuông góc với trục chính).
- Kích thước (size): main size (kích thước theo trục chính) và cross size (kích thước theo trục phụ).
- Các điểm (point): main start, main end (đầu và cuối trục chính), cross start, cross end (đầu và cuối trục phụ).
flex-direction: rowflex-direction: columnjustify-content: flex-startcenterflex-endspace-betweenspace-aroundspace-evenlycăn theo main axisalign-items: flex-startcenterflex-endstretchcăn theo cross axisjustify-content căn theo trục chính (main axis), align-items căn theo trục phụ (cross axis). Đổi flex-direction thì hai trục đổi chỗ cho nhau.
Hình trên cho thấy một container với hai trục. Mặc định (flex-direction: row), trục chính chạy ngang từ trái sang phải, trục phụ chạy dọc từ trên xuống dưới. Hãy nhớ một quy tắc rất hữu ích: justify-content luôn làm việc trên trục chính, còn align-items luôn làm việc trên trục phụ. Khi bạn đổi hướng thành column, hai trục đổi chỗ cho nhau, và hai thuộc tính cũng "đổi chiều" theo.
Giáo trình: Chương 4 — mục "Phần tử chứa (Flex container)". Giáo trình liệt kê sáu thuộc tính cho container:
flex-direction,flex-wrap,flex-flow,align-items,justify-content,align-content.
1.2. Phần tử chứa (Flex container)
Để bắt đầu, ta xác định một flex container bằng display: flex. Sáu thuộc tính dưới đây chỉ có tác dụng khi đặt trên container (không đặt trên item).
| Thuộc tính | Việc nó điều khiển | Giá trị thường dùng |
|---|---|---|
flex-direction |
Hướng của main axis | row (mặc định), column, row-reverse, column-reverse |
flex-wrap |
Có xuống dòng khi thiếu chỗ không | nowrap (mặc định), wrap, wrap-reverse |
flex-flow |
Viết tắt của flex-direction + flex-wrap |
column nowrap, row wrap |
justify-content |
Căn các item theo main axis | flex-start, flex-end, center, space-between, space-around, space-evenly |
align-items |
Căn các item theo cross axis | stretch, center, flex-start, flex-end, baseline |
align-content |
Căn các dòng (line) theo cross axis | stretch, flex-start, flex-end, center, space-between, space-around |
flex-direction
Thuộc tính flex-direction xác định hướng sắp xếp các flex item trong container, tức là hướng của main-axis. Các giá trị:
- row (mặc định): các phần tử xếp trên một hàng, từ trái sang phải.
- column: các phần tử xếp trên một cột, từ trên xuống dưới.
- row-reverse: xếp trên một hàng theo chiều từ phải sang trái (nếu chữ viết từ trái sang phải).
- column-reverse: xếp thành một cột theo chiều từ dưới lên trên.
Ví dụ: khi đặt flex-direction: column, các item nằm theo chiều thẳng đứng. Đoạn mã dưới tạo bốn container với bốn hướng khác nhau để bạn so sánh. Chú ý thứ tự chữ "1, 2, 3" trong từng hộp.
<style>
body { font-family: Arial; font-size: 14px; }
.box { display: flex; background: #1abc9c; margin-bottom: 6px; padding: 4px; gap: 4px; }
.box div { background: white; padding: 6px 14px; }
.col { height: 90px; width: 100px; display: inline-flex; }
.wrap { display: flex; gap: 16px; align-items: flex-start; }
</style>
<p><b>row</b></p>
<div class="box" id="b1" style="flex-direction: row"><div>1</div><div>2</div><div>3</div></div>
<p><b>row-reverse</b></p>
<div class="box" id="b2" style="flex-direction: row-reverse"><div>1</div><div>2</div><div>3</div></div>
<div class="wrap">
<div><p><b>column</b></p>
<div class="box" id="b3" style="flex-direction: column; width: 100px"><div>1</div><div>2</div><div>3</div></div></div>
<div><p><b>column-reverse</b></p>
<div class="box" id="b4" style="flex-direction: column-reverse; width: 100px"><div>1</div><div>2</div><div>3</div></div></div>
</div>
<script>
["b1", "b2", "b3", "b4"].forEach((id) => {
const el = document.getElementById(id);
console.log(id + ": flex-direction =", getComputedStyle(el).flexDirection);
});
</script>Mẹo:
row-reversevàcolumn-reversechỉ đảo chiều hiển thị, không đổi thứ tự trong mã HTML. Trình đọc màn hình và phím Tab vẫn đi theo thứ tự HTML, nên đừng dùng đảo chiều để sắp xếp nội dung quan trọng theo kiểu "lệch" so với logic đọc.
flex-wrap và flex-flow
Thuộc tính flex-wrap kiểm soát việc các item hiển thị thế nào khi kích thước theo main-axis không đủ. Các giá trị:
- nowrap (mặc định): các item luôn nằm trên một hàng (hoặc một cột) duy nhất, bị ép co lại nếu thiếu chỗ.
- wrap: các item xuất hiện trên nhiều hàng, nhiều cột; item nào không vừa sẽ chuyển xuống dòng mới.
- wrap-reverse: giống
wrapnhưng thứ tự các hàng (hoặc cột) bị đảo.
Thuộc tính flex-flow là viết tắt (shorthand) để thiết lập cả flex-direction và flex-wrap, ví dụ flex-flow: column nowrap; hoặc flex-flow: row wrap;. Ví dụ sau có 8 item mỗi item rộng 120px trong container rộng 400px. Bạn hãy đoán: khi nowrap thì chuyện gì xảy ra với độ rộng 120px? Console in ra giá trị flex-wrap của hai container, còn phần xem trước cho thấy sự khác biệt.
<style>
body { font-family: Arial; font-size: 13px; }
.fc { display: flex; width: 400px; background: tomato; margin-bottom: 8px; }
.fc div { width: 120px; background: #fff3; border: 1px solid #fff; padding: 8px 0; text-align: center; }
</style>
<p>flex-wrap: nowrap (mặc định) — item bị co lại để vừa một hàng</p>
<div class="fc" id="f1" style="flex-wrap: nowrap"><div>1</div><div>2</div><div>3</div><div>4</div><div>5</div><div>6</div></div>
<p>flex-wrap: wrap — item giữ 120px và xuống dòng</p>
<div class="fc" id="f2" style="flex-wrap: wrap"><div>1</div><div>2</div><div>3</div><div>4</div><div>5</div><div>6</div></div>
<script>
console.log("f1:", getComputedStyle(document.getElementById("f1")).flexWrap);
console.log("f2:", getComputedStyle(document.getElementById("f2")).flexWrap);
console.log("Tổng độ rộng yêu cầu: 6 x 120 =", 6 * 120, "px; container chỉ có 400px");
</script>align-items
Thuộc tính align-items được dùng để co giãn và căn chỉnh các item theo cross-axis. Các giá trị:
- stretch (mặc định): các phần tử được giãn 100% theo trục cross (nếu item chưa đặt chiều cao).
- center: các phần tử nằm giữa.
- flex-start: các phần tử ở vị trí cross-start.
- flex-end: các phần tử ở vị trí cross-end.
- baseline: các phần tử được căn thẳng theo đường cơ sở (baseline) của chữ.
Khi muốn căn giữa các phần tử đang hiển thị dạng hàng (row) theo chiều dọc, dùng align-items: center và cho container một chiều cao (ví dụ height: 200px), vì nếu container chỉ cao vừa đủ nội dung thì không có "chỗ thừa" để căn. Ví dụ dưới đây cho bạn bấm đổi giá trị bằng JavaScript để quan sát (console in ra giá trị đã đặt).
<style>
body { font-family: Arial; font-size: 13px; }
.fc { display: flex; background: tomato; height: 150px; gap: 6px; padding: 4px; }
.fc div { background: white; padding: 6px 12px; }
.fc div:nth-child(2) { font-size: 26px; }
button { margin: 0 4px 8px 0; padding: 4px 10px; cursor: pointer; }
</style>
<div id="bar"></div>
<div class="fc" id="fc"><div>Nhỏ</div><div>Lớn</div><div>Vừa</div></div>
<script>
const fc = document.getElementById("fc");
for (const v of ["stretch", "center", "flex-start", "flex-end", "baseline"]) {
const b = document.createElement("button");
b.textContent = v;
b.onclick = () => { fc.style.alignItems = v; console.log("align-items =", fc.style.alignItems); };
document.getElementById("bar").appendChild(b);
}
fc.style.alignItems = "center";
console.log("align-items ban đầu =", fc.style.alignItems);
</script>justify-content
Thuộc tính justify-content được dùng để sắp xếp các phần tử theo main-axis khi còn thừa không gian. Các giá trị:
- flex-start (mặc định): dồn nội dung sang vị trí main-start.
- flex-end: dồn nội dung sang vị trí main-end.
- center: căn giữa.
- space-around: khoảng cách giữa các item bằng nhau (mỗi item có nửa khoảng trống mỗi bên).
- space-between: item đầu sát mép đầu, item cuối sát mép cuối, phần không gian thừa chia đều giữa các item.
- space-evenly: phân bố đều với khoảng trống bằng nhau giữa các phần tử, bao gồm cả đầu và cuối container.
Giáo trình mô tả space-around là "khoảng cách giữa các item bằng nhau" và space-between là "không gian thừa cho mỗi item bằng nhau"; cách hiểu chính xác hơn là: space-between không để chừa mép hai đầu, space-around chừa nửa khoảng ở mỗi đầu, space-evenly chừa trọn một khoảng ở mỗi đầu. Đoạn mã sau tính ra vị trí bắt đầu của từng item cho cả ba kiểu để bạn kiểm chứng bằng số.
// Container rộng 600px, 3 item mỗi item 100px => không gian thừa = 300px
const W = 600, w = 100, n = 3;
const free = W - n * w;
function viTri(kieu) {
let dau = 0, giua = 0;
if (kieu === "flex-start") { dau = 0; giua = 0; }
if (kieu === "flex-end") { dau = free; giua = 0; }
if (kieu === "center") { dau = free / 2; giua = 0; }
if (kieu === "space-between") { dau = 0; giua = free / (n - 1); }
if (kieu === "space-around") { giua = free / n; dau = giua / 2; }
if (kieu === "space-evenly") { giua = free / (n + 1); dau = giua; }
return Array.from({ length: n }, (_, i) => dau + i * (w + giua));
}
for (const k of ["flex-start", "flex-end", "center", "space-between", "space-around", "space-evenly"]) {
console.log(k.padEnd(14), "-> các item bắt đầu tại x =", viTri(k).map((x) => +x.toFixed(1)).join(", "));
}Mẹo: Muốn nhóm "logo bên trái, menu bên phải" hãy dùng
justify-content: space-betweenvới hai con. Muốn căn giữa một hộp theo cả hai chiều:display: flex; justify-content: center; align-items: center;và cho container một chiều cao. Đây là công thức "căn giữa" gọn nhất hiện nay.
align-content
Thuộc tính align-content sắp xếp các dòng (line) theo cross-axis khi còn thừa không gian. Nó chỉ có tác dụng khi các item nằm trên nhiều dòng, tức là khi flex-wrap là wrap hoặc wrap-reverse. Các giá trị: stretch (mặc định, dãn hết kích thước), flex-end, flex-start, center, space-around, space-between. Ví dụ giáo trình: container cao 300px, flex-wrap: wrap, align-content: center làm các hàng nằm giữa container; hoặc container cột cao 600px với flex-direction: column; flex-wrap: wrap; align-content: flex-end làm các cột dồn sang phải.
Lưu ý: Hãy phân biệt:
align-itemscăn từng item trong dòng của nó, cònalign-contentcăn cả các dòng trong container. Nếu chỉ có một dòng thìalign-contentkhông có tác dụng — lỗi rất hay gặp khi quên bậtflex-wrap: wrap.
<style>
body { font-family: Arial; font-size: 13px; }
.fc { display: flex; flex-wrap: wrap; width: 300px; height: 200px; background: tomato; }
.fc div { width: 90px; height: 40px; margin: 3px; background: white; text-align: center; line-height: 40px; }
</style>
<div class="fc" id="fc" style="align-content: center">
<div>1</div><div>2</div><div>3</div><div>4</div><div>5</div><div>6</div>
</div>
<script>
const fc = document.getElementById("fc");
const s = getComputedStyle(fc);
console.log("flex-wrap:", s.flexWrap, "| align-content:", s.alignContent, "| height:", s.height);
</script>Bố cục linh hoạt (Flexbox): container và item
đổi thuộc tính, bấm một item để chỉnh riêng nó.container { display: flex; gap: 8px; }
Phòng thí nghiệm Flexbox trên đây cho phép bạn chọn từng thuộc tính của container và xem các item di chuyển ngay. Cách đọc: đặt flex-direction trước, sau đó thử justify-content (trục chính) và align-items (trục phụ); khi chuyển từ row sang column, hãy chú ý hai thuộc tính này "đổi vai" thế nào. Giáo trình cũng có công cụ "Flexbox trực quan" cùng mục đích tại fit.neu.edu.vn.
Vừa học xong các thuộc tính của flex container, hãy làm các bài dưới đây để luyện: xếp menu thành cột, căn nút, cho các thẻ tự xuống dòng và dựng một thanh công cụ có ô tìm kiếm co giãn.
1.3. Phần tử con (Flex item)
Giáo trình: Chương 4 — mục "Phần tử con (Flex item)". Giáo trình liệt kê sáu thuộc tính của item:
order,flex-grow,flex-shrink,flex-basis,flex,align-self.
Các phần tử con trong container có thể thiết lập thêm sáu thuộc tính sau.
| Thuộc tính | Ý nghĩa | Mặc định |
|---|---|---|
order |
Xác định thứ tự hiển thị của item | 0 |
flex-grow |
Mức độ giãn ra (grow) khi container còn thừa không gian | 0 |
flex-shrink |
Mức độ co lại (shrink) khi container thiếu không gian | 1 |
flex-basis |
Kích thước ban đầu theo main axis | auto |
flex |
Viết tắt của flex-grow, flex-shrink, flex-basis |
0 1 auto |
align-self |
Căn riêng một item theo cross axis | auto |
order
Thuộc tính order xác định thứ tự của các phần tử: item có order nhỏ hơn đứng trước, các item bằng nhau giữ thứ tự trong HTML. Ví dụ giáo trình: ba item có order: 3, order: 2, order: 1 sẽ hiển thị theo thứ tự "Item 3, Item 2, Item 1" dù trong mã HTML chúng nằm theo thứ tự ngược lại. Đoạn mã dưới sắp xếp lại bằng JavaScript theo đúng quy tắc đó để bạn thấy bản chất: một phép sắp xếp ổn định theo order.
<style>
body { font-family: Arial; }
.flex-container { display: flex; background: #1abc9c; padding: 4px; gap: 4px; }
.item { background: white; padding: 18px 24px; }
</style>
<div class="flex-container" id="fc">
<div class="item" style="order: 3">Item 1</div>
<div class="item" style="order: 2">Item 2</div>
<div class="item" style="order: 1">Item 3</div>
</div>
<script>
const items = [...document.querySelectorAll("#fc .item")];
const sorted = items
.map((el, i) => ({ ten: el.textContent, order: Number(el.style.order), vt: i }))
.sort((a, b) => a.order - b.order || a.vt - b.vt);
console.log("Thứ tự trong HTML:", items.map((e) => e.textContent).join(" > "));
console.log("Thứ tự hiển thị :", sorted.map((o) => o.ten).join(" > "));
</script>flex-grow
Thuộc tính flex-grow xác định các item phát triển (grow) theo tỷ lệ so với các item khác khi thừa không gian. Giá trị phải là số (mặc định 0, nghĩa là không giãn). Với ví dụ flex-grow lần lượt là 1, 3, 1, phần thừa của container được chia theo tỷ lệ 1 : 3 : 1, nên item giữa được thêm gấp ba lần mỗi item bên cạnh. Điều quan trọng là: flex-grow chia phần thừa, không chia toàn bộ chiều rộng, nên kích thước cuối cùng còn phụ thuộc vào flex-basis ban đầu của mỗi item. Hàm chiaFlex bên dưới mô phỏng đúng công thức mà trình duyệt dùng; bạn sẽ dùng lại nó để kiểm chứng flex-shrink ngay sau đây.
// Mô phỏng cách Flexbox chia không gian trên main axis (rút gọn, bỏ qua min/max)
function chiaFlex(W, items) {
const tongBasis = items.reduce((s, it) => s + it.basis, 0);
const free = W - tongBasis;
if (free >= 0) {
const tongGrow = items.reduce((s, it) => s + it.grow, 0);
return items.map((it) => it.basis + (tongGrow ? (free * it.grow) / tongGrow : 0));
}
const tongShrink = items.reduce((s, it) => s + it.shrink * it.basis, 0);
return items.map((it) => it.basis + (tongShrink ? (free * it.shrink * it.basis) / tongShrink : 0));
}
// Ví dụ giáo trình: flex-grow 1, 3, 1 trong container 600px
console.log("basis 0, grow 1:3:1 ->", chiaFlex(600, [
{ basis: 0, grow: 1, shrink: 1 }, { basis: 0, grow: 3, shrink: 1 }, { basis: 0, grow: 1, shrink: 1 },
]));
// Cùng grow nhưng mỗi item có basis 100px: chỉ phần thừa 300px được chia theo 1:3:1
console.log("basis 100, grow 1:3:1 ->", chiaFlex(600, [
{ basis: 100, grow: 1, shrink: 1 }, { basis: 100, grow: 3, shrink: 1 }, { basis: 100, grow: 1, shrink: 1 },
]));Mẹo: Muốn các cột bằng nhau tuyệt đối hãy đặt
flex: 1(nghĩa là1 1 0%) cho tất cả: basis bằng 0 nên toàn bộ chiều rộng được chia theo tỷ lệ grow. Nếu chỉ đặtflex-grow: 1mà đểflex-basis: auto, các cột có nội dung dài ngắn khác nhau sẽ ra chiều rộng không bằng nhau.
flex-shrink
Thuộc tính flex-shrink xác định các item co (shrink) theo tỷ lệ so với các item khác khi không đủ không gian. Giá trị phải là số, mặc định là 1 (tức là mặc định item được phép co). Ví dụ giáo trình: ba div mỗi cái width: 200px trong container 500px; để item thứ hai không bị co, đặt flex-shrink: 0 cho nó. Ba item cần 600px nhưng container chỉ có 500px, thiếu 100px; hai item còn lại cùng gánh phần thiếu đó. Hãy chạy thử: kết quả 150 / 200 / 150.
function chiaFlex(W, items) {
const tongBasis = items.reduce((s, it) => s + it.basis, 0);
const free = W - tongBasis;
if (free >= 0) {
const tongGrow = items.reduce((s, it) => s + it.grow, 0);
return items.map((it) => it.basis + (tongGrow ? (free * it.grow) / tongGrow : 0));
}
const tongShrink = items.reduce((s, it) => s + it.shrink * it.basis, 0);
return items.map((it) => it.basis + (tongShrink ? (free * it.shrink * it.basis) / tongShrink : 0));
}
const W = 500;
console.log("Cả ba đều shrink 1 :", chiaFlex(W, [
{ basis: 200, grow: 0, shrink: 1 }, { basis: 200, grow: 0, shrink: 1 }, { basis: 200, grow: 0, shrink: 1 },
]).map((x) => +x.toFixed(1)));
console.log("Item 2 shrink 0 :", chiaFlex(W, [
{ basis: 200, grow: 0, shrink: 1 }, { basis: 200, grow: 0, shrink: 0 }, { basis: 200, grow: 0, shrink: 1 },
]).map((x) => +x.toFixed(1)));Mẹo: Ảnh đại diện hoặc logo bên cạnh một đoạn chữ dài thường bị bóp méo khi không đủ chỗ. Đặt
flex-shrink: 0cho logo/avatar để nó giữ nguyên kích thước và để phần chữ tự co hoặc xuống dòng.
flex-basis
Thuộc tính flex-basis xác định kích thước ban đầu của một item theo chiều main-axis, trước khi giãn hoặc co. Nó tương tự width (khi trục chính nằm ngang) hoặc height (khi trục chính thẳng đứng); nếu cả hai cùng có, flex-basis được ưu tiên (trừ giá trị auto thì dùng width/height). Ví dụ giáo trình: ba item có flex-basis lần lượt 100px, 200px, 300px.
flex (viết tắt)
Thuộc tính flex là viết tắt của flex-grow, flex-shrink và flex-basis theo đúng thứ tự đó. Ví dụ giáo trình: item 2 có flex: 0 0 200px (không giãn, không co, rộng đúng 200px), item 3 có flex: 1 0 0 (không co nhưng được giãn để chiếm toàn bộ phần còn lại). Bảng dưới là các kiểu viết ngắn thường gặp.
| Khai báo | Tương đương | Ý nghĩa dùng thực tế |
|---|---|---|
flex: 1 |
1 1 0% |
Chia đều không gian, mỗi item bằng nhau |
flex: 2 |
2 1 0% |
Chiếm gấp đôi flex: 1 |
flex: auto |
1 1 auto |
Giãn theo nội dung rồi chia phần thừa |
flex: none |
0 0 auto |
Giữ nguyên kích thước, không giãn, không co |
flex: 0 0 200px |
(đã đầy đủ) | Cột cố định 200px |
<style>
body { font-family: Arial; font-size: 14px; }
.flex-container { display: flex; background: tomato; padding: 4px; gap: 4px; width: 560px; }
.item { background: white; padding: 14px; }
</style>
<div class="flex-container" id="fc">
<div class="item">Item 1 (tự nhiên)</div>
<div class="item" id="i2" style="flex: 0 0 200px">Item 2: flex 0 0 200px</div>
<div class="item" id="i3" style="flex: 1 0 0">Item 3: flex 1 0 0</div>
</div>
<p>Container rộng 560px. Item 2 cố định 200px, Item 3 lấy phần còn lại.</p>
<script>
for (const id of ["i2", "i3"]) {
const el = document.getElementById(id);
console.log(id + " -> grow:", el.style.flexGrow, "| shrink:", el.style.flexShrink, "| basis:", el.style.flexBasis);
}
</script>align-self
Thuộc tính align-self chỉ định căn riêng một item theo cross axis, ghi đè align-items của container. Các giá trị: auto (mặc định, theo align-items của container; nếu container không đặt thì là stretch), flex-start, flex-end, center, baseline (và stretch). Ví dụ giáo trình dùng một container cao 120px chứa sáu div với các giá trị align-self khác nhau, trong đó hai phần tử cuối cùng cùng baseline nhưng một phần tử có font-size: 1.5em để thấy các chữ vẫn cùng đường cơ sở.
<style>
body { font-family: Arial; font-size: 13px; }
.flex-container1 { display: flex; height: 120px; background: #ffe0b2; gap: 4px; }
.flex-container1 > div { background: #1abc9c; color: white; padding: 4px 8px; }
</style>
<div class="flex-container1" id="fc">
<div style="align-self: stretch">stretch</div>
<div style="align-self: center">center</div>
<div style="align-self: flex-start">flex-start</div>
<div style="align-self: flex-end">flex-end</div>
<div style="align-self: baseline">baseline</div>
<div style="align-self: baseline; font-size: 1.5em">baseline</div>
</div>
<script>
[...document.querySelectorAll("#fc > div")].forEach((el, i) =>
console.log((i + 1) + ". align-self =", getComputedStyle(el).alignSelf));
</script>Cảnh báo:
align-selfcăn theo cross axis; muốn đẩy riêng một item theo main axis (ví dụ nút "Đăng nhập" dính phải) hãy dùngmargin-left: auto, vì lềautotrong flex container hấp thụ toàn bộ không gian thừa. Không có thuộc tínhjustify-selfcho flex item.
Vừa học xong các thuộc tính của flex item, hãy làm các bài sau để luyện chia cột theo tỉ lệ, giữ logo không bị co, đảo thứ tự bằng order và làm các thẻ cao bằng nhau với nút luôn nằm đáy.
2. Bố cục lưới (Grid)
Giáo trình: Chương 4 — mục "Bố cục lưới", gồm ba mục nhỏ: "Khái niệm về lưới (Grid)", "Đơn vị fr và các hàm repeat(), minmax()" và "Phần tử con (Grid item)".
2.1. Khái niệm về lưới (Grid)
Bố cục lưới (grid layout) trong CSS là một cách tiếp cận mạnh mẽ để thiết kế và tổ chức các phần tử trên trang Web hoặc ứng dụng Web. Bố cục lưới cho phép chia không gian trang thành các ô lưới (grid cells) có thể điều chỉnh được, nơi bạn định vị các phần tử (ô nhập liệu, nút, văn bản, hình ảnh...) một cách dễ dàng và linh hoạt. Giống Flexbox, bố cục lưới có một phần tử cha (container) và một hoặc nhiều phần tử con (item).
Khác biệt cốt lõi: Flexbox là bố cục một chiều (một hàng hoặc một cột tại một thời điểm), còn Grid là bố cục hai chiều (đồng thời kiểm soát cả hàng và cột). Bạn dùng Flexbox cho thanh menu, nhóm nút, hàng thẻ; dùng Grid cho cả trang hoặc vùng có cả hàng lẫn cột phải thẳng hàng với nhau.
Ví dụ đầu tiên của giáo trình: một container và sáu item, hai cột (auto auto), do đó có ba hàng. Bố cục này dễ dàng đổi thành 3 cột – 2 hàng, 1 cột – 6 hàng, hay 6 cột – 1 hàng chỉ bằng cách sửa grid-template-columns. Hãy chạy và đếm số cột mà trình duyệt ghi nhận.
<style>
.grid-container { display: grid; grid-template-columns: auto auto; }
.grid-item { background-color: antiquewhite; border: 1px solid rgba(0, 0, 0, 0.8); padding: 6px; margin: 4px; font-size: 22px; text-align: center; color: black; }
</style>
<div class="grid-container" id="g">
<div class="grid-item">1</div><div class="grid-item">2</div>
<div class="grid-item">3</div><div class="grid-item">4</div>
<div class="grid-item">5</div><div class="grid-item">6</div>
</div>
<script>
const g = document.getElementById("g");
const cols = getComputedStyle(g).gridTemplateColumns.split(/\s+/).length;
console.log("display:", getComputedStyle(g).display);
console.log("grid-template-columns:", getComputedStyle(g).gridTemplateColumns);
console.log("Số item:", g.children.length, "-> số cột khai báo:", cols, "-> số hàng:", Math.ceil(g.children.length / cols));
</script>Một grid container được tạo bằng display: grid. Giáo trình liệt kê các thuộc tính khác có thể thiết lập: grid-template-columns, grid-template-rows, grid-template, grid-template-areas, justify-content, align-content, grid-column-gap, grid-row-gap, grid-gap (và nhóm grid-column-start, grid-column-end, grid-row-start, grid-row-end dùng cho item, sẽ học ở mục 2.3).
.page { display: grid; grid-template-columns: 1fr 3fr 1fr; grid-template-rows: auto 1fr auto; grid-template-areas: "header header header" "nav main aside" "footer footer footer"; gap: 8px; } header { grid-area: header; } nav { grid-area: nav; } /* … main, aside, footer tương tự */
Dấu . trong chuỗi vùng là ô trống; gap là khoảng cách giữa các hàng và cột.
Chia container thành các hàng và cột, đặt tên cho từng vùng bằng grid-template-areas rồi gán grid-area cho phần tử: bố cục hiện ra ngay trong chuỗi mã.
Hình trên cho thấy một lưới gồm các đường lưới (grid lines) đánh số từ 1, các hàng, các cột và các vùng (areas). Hãy nhớ: một lưới có n cột thì có n + 1 đường dọc; item được đặt "từ đường này đến đường kia", đây là chìa khóa để hiểu grid-column: 1 / 3.
grid-template-columns và grid-template-rows
Thuộc tính grid-template-columns xác định số cột và độ rộng của từng cột bằng danh sách giá trị cách nhau bằng dấu cách. Ví dụ ba cột kích thước tự động: grid-template-columns: auto auto auto;. Cột đầu 150px, cột hai 200px: grid-template-columns: 150px 200px;. Thuộc tính grid-template-rows xác định chiều cao từng hàng; ví dụ grid-template-columns: auto auto auto auto; kết hợp grid-template-rows: 100px 300px; cho bốn cột và hàng đầu cao 100px, hàng hai cao 300px.
<style>
.g { display: grid; background: beige; margin-bottom: 8px; }
.g div { background: antiquewhite; border: 1px solid #555; padding: 6px; text-align: center; }
</style>
<div class="g" id="g1" style="grid-template-columns: 150px 200px;"><div>1</div><div>2</div><div>3</div><div>4</div></div>
<div class="g" id="g2" style="grid-template-columns: auto auto auto auto; grid-template-rows: 40px 90px;"><div>1</div><div>2</div><div>3</div><div>4</div><div>5</div><div>6</div><div>7</div><div>8</div></div>
<script>
for (const id of ["g1", "g2"]) {
const s = getComputedStyle(document.getElementById(id));
console.log(id, "-> cột:", s.gridTemplateColumns, "| hàng:", s.gridTemplateRows || "(tự động)");
}
</script>grid-template
Thuộc tính grid-template xác định số hàng, số cột và kích thước của chúng trong một thuộc tính duy nhất; đây là cách viết rút gọn của grid-template-rows và grid-template-columns. Cú pháp: các hàng trước dấu /, các cột sau dấu /. Ví dụ grid-template: 150px 250px / auto auto auto; tạo 2 hàng (150px, 250px) và 3 cột.
.grid-container {
display: grid;
grid-template: 150px 250px / auto auto auto; /* hàng / cột */
}grid-template-areas
Thuộc tính grid-template-areas xác định các vùng trong grid-layout, các vùng này được dùng để tạo ra bố cục. Mỗi chuỗi là một hàng, mỗi từ trong chuỗi là tên vùng của một cột; dấu chấm . nghĩa là ô trống. Item "nhận" vùng bằng thuộc tính grid-area: tên-vùng. Ví dụ giáo trình đặt .item1 { grid-area: myArea; } và vùng myArea chiếm bốn ô (2 cột × 2 hàng) còn cột thứ ba để trống.
Hàm dưới đây đọc chuỗi grid-template-areas và tính vị trí (đường lưới bắt đầu/kết thúc) của từng vùng — chính là cách trình duyệt quy đổi tên vùng ra grid-area: hàng-đầu / cột-đầu / hàng-cuối / cột-cuối.
// Đọc grid-template-areas và đổi mỗi vùng thành đường lưới
const areas = [
"header header header header header header",
"menu main main main right right",
"menu footer footer footer footer footer",
];
function docVung(rows) {
const out = {};
rows.forEach((row, r) => {
row.trim().split(/\s+/).forEach((ten, c) => {
if (ten === ".") return;
const v = (out[ten] ||= { rowStart: r + 1, rowEnd: r + 2, colStart: c + 1, colEnd: c + 2 });
v.rowEnd = Math.max(v.rowEnd, r + 2);
v.colEnd = Math.max(v.colEnd, c + 2);
});
});
return out;
}
const vung = docVung(areas);
for (const [ten, v] of Object.entries(vung)) {
console.log(ten.padEnd(7), `grid-area: ${v.rowStart} / ${v.colStart} / ${v.rowEnd} / ${v.colEnd}`);
}justify-content và align-content (trong Grid)
Khi tổng kích thước các cột nhỏ hơn container, bạn có thể căn cả lưới trong container. justify-content căn các cột theo chiều ngang với các giá trị stretch (mặc định), center, start, end, space-evenly, space-around, space-between. align-content căn toàn bộ các hàng theo chiều dọc trong container (cần container cao hơn tổng chiều cao các hàng). Giáo trình mô tả các giá trị:
- stretch: giá trị mặc định, các dòng kéo căng để lấp đầy container theo chiều dọc.
- center: các dòng căn giữa theo chiều dọc.
- start: các dòng căn về phía đầu container (bên trên nếu hướng từ trên xuống).
- end: các dòng căn về phía cuối container (bên dưới).
- space-between: các dòng phân bố đều trong container.
- space-around: giống
space-betweennhưng có thêm khoảng cách ở hai đầu. - space-evenly: khoảng cách đều nhau giữa các dòng và cả ở đầu và cuối.
<style>
.grid-container { display: grid; background: beige; grid-template-columns: 60px 60px 60px; height: 240px; }
.grid-container div { background: antiquewhite; border: 1px solid #555; padding: 8px; text-align: center; }
</style>
<div class="grid-container" id="g" style="justify-content: space-evenly; align-content: end">
<div>1</div><div>2</div><div>3</div><div>4</div><div>5</div><div>6</div>
</div>
<script>
const s = getComputedStyle(document.getElementById("g"));
console.log("justify-content:", s.justifyContent, "| align-content:", s.alignContent);
</script>Khoảng cách giữa các ô: gap
Các thuộc tính có tên kết thúc bằng -gap thiết lập khoảng cách giữa các hàng và cột trong lưới. grid-column-gap đặt khoảng cách giữa các cột, grid-row-gap đặt khoảng cách giữa các dòng, còn grid-gap là viết tắt của cả hai: grid-gap: 50px 100px; nghĩa là 50px giữa các hàng và 100px giữa các cột (thứ tự: hàng trước, cột sau).
Lưu ý: Giáo trình ghi chú rằng các tên có tiền tố
grid-(grid-gap,grid-column-gap,grid-row-gap) vẫn chạy được nhưng đã lỗi thời. Mã mới nên dùnggap(hoặcrow-gapvàcolumn-gap).gapcòn dùng được trong Flexbox, nên đáng nhớ hơn.
2.2. Đơn vị fr và các hàm repeat(), minmax()
Ngoài các giá trị cố định như pixel hay auto, lưới còn có một đơn vị riêng là fr (fraction — phần chia). Đơn vị này chia phần không gian còn trống của vùng chứa theo tỷ lệ, nhờ đó lưới tự co giãn theo kích thước màn hình mà không cần tính toán bằng tay. Ví dụ grid-template-columns: 1fr 2fr 1fr; cho cột giữa rộng gấp đôi hai cột hai bên.
Khi các cột có kích thước giống nhau, dùng hàm repeat() để khỏi viết lặp lại: repeat(4, 1fr) tương đương 1fr 1fr 1fr 1fr. Hàm minmax(min, max) quy định kích thước nhỏ nhất và lớn nhất của một cột. Kết hợp với từ khóa auto-fit, ta có một lưới tự động đổi số cột theo chiều rộng màn hình mà không cần viết media query nào: grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));.
Hai đoạn mã sau giúp bạn "nhìn thấy" các phép tính. Thứ nhất: fr chia phần còn lại sau khi trừ gap.
// Chia chiều rộng cho các cột fr: (container - tổng gap - cột cố định) / tổng fr
function chiaFr(W, gap, cot) {
const nCot = cot.length;
const codinh = cot.filter((c) => c.px).reduce((s, c) => s + c.px, 0);
const tongFr = cot.reduce((s, c) => s + (c.fr || 0), 0);
const conLai = W - gap * (nCot - 1) - codinh;
return cot.map((c) => (c.px ? c.px : +((conLai * c.fr) / tongFr).toFixed(2)));
}
console.log("1fr 2fr 1fr, 900px, gap 20px ->", chiaFr(900, 20, [{ fr: 1 }, { fr: 2 }, { fr: 1 }]));
console.log("repeat(4, 1fr), 1000px, gap 0 ->", chiaFr(1000, 0, [{ fr: 1 }, { fr: 1 }, { fr: 1 }, { fr: 1 }]));
console.log("200px 1fr, 800px, gap 16px ->", chiaFr(800, 16, [{ px: 200 }, { fr: 1 }]));Thứ hai: repeat(auto-fit, minmax(200px, 1fr)) chọn số cột lớn nhất mà mỗi cột vẫn còn ít nhất 200px, rồi chia đều phần dư. Bạn có thể thấy lưới "tự đổi" 4 → 3 → 2 → 1 cột khi màn hình hẹp dần mà không có @media nào.
// Số cột mà repeat(auto-fit, minmax(min, 1fr)) tạo ra
function autoFit(W, min, gap) {
const n = Math.max(1, Math.floor((W + gap) / (min + gap)));
const w = (W - (n - 1) * gap) / n;
return { cot: n, rongMoiCot: +w.toFixed(1) };
}
for (const W of [1200, 1000, 700, 500, 360]) {
const r = autoFit(W, 200, 16);
console.log(`màn hình ${W}px -> ${r.cot} cột, mỗi cột ${r.rongMoiCot}px`);
}Mở rộng:
auto-fitvàauto-fillđều tạo số cột như trên. Khác biệt chỉ lộ ra khi có ít item hơn số cột:auto-fillgiữ nguyên các cột trống (item giữ cỡ 200px), cònauto-fitthu các cột trống về 0 để item giãn ra chiếm hết chỗ. Với danh sách thẻ sản phẩm,auto-fitthường là lựa chọn mong muốn.
<style>
body { font-family: Arial; margin: 6px; }
.grid-container { display: grid; grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); gap: 10px; }
.grid-container div { background: #1abc9c; color: white; padding: 18px 6px; text-align: center; border-radius: 6px; }
</style>
<p>Kéo thay đổi độ rộng khung xem trước: số cột tự đổi.</p>
<div class="grid-container" id="g">
<div>Thẻ 1</div><div>Thẻ 2</div><div>Thẻ 3</div><div>Thẻ 4</div><div>Thẻ 5</div><div>Thẻ 6</div>
</div>
<script>
const s = getComputedStyle(document.getElementById("g"));
console.log("grid-template-columns:", s.gridTemplateColumns);
console.log("gap:", s.gap);
console.log("Số thẻ:", document.querySelectorAll("#g div").length);
</script>2.3. Phần tử con (Grid item)
Trong container, các phần tử con có thể được đặt vị trí và kích thước bằng các thuộc tính: grid-column-start, grid-column-end, grid-row-start, grid-row-end, grid-column, grid-row, grid-area.
| Thuộc tính | Ý nghĩa | Ví dụ |
|---|---|---|
grid-column-start / grid-column-end |
Đường lưới bắt đầu / kết thúc theo cột | grid-column-start: 1; grid-column-end: 4 |
grid-row-start / grid-row-end |
Đường lưới bắt đầu / kết thúc theo hàng | grid-row-start: 1; grid-row-end: 4 |
grid-column |
Viết tắt: cột bắt đầu / kết thúc (hoặc span) |
grid-column: 1 / 3, grid-column: 1 / span 3 |
grid-row |
Viết tắt: hàng bắt đầu / kết thúc | grid-row: 2 / span 2 |
grid-area |
Viết tắt bốn đường, hoặc tên vùng | grid-area: 1 / 2 / 4 / 3, grid-area: header |
Trong ví dụ giáo trình, item 1 có grid-column-start: 1; grid-column-end: 4 nên trải từ đường dọc 1 đến đường dọc 4, tức là qua cả ba cột. Tương tự, grid-row-start: 1; grid-row-end: 4 làm item 1 trải qua ba hàng. Thuộc tính grid-column: 1 / 3 viết gọn hai số "bắt đầu / kết thúc"; còn grid-column: 1 / span 3 nghĩa là "bắt đầu ở đường 1 và kéo dài 3 cột". Thuộc tính grid-row: 2 / span 2 xác định vị trí theo chiều dọc. Cuối cùng grid-area: 1 / 2 / 4 / 3 là viết tắt của grid-row-start / grid-column-start / grid-row-end / grid-column-end (chú ý thứ tự: hàng-đầu, cột-đầu, hàng-cuối, cột-cuối).
Bạn hãy bấm chạy: ba ví dụ lưới 3 cột với sáu item, mỗi ví dụ một kiểu đặt vị trí. Phần console đọc lại giá trị đặt cho item.
<style>
.grid-container { display: grid; grid-template-columns: auto auto auto; background: beige; margin-bottom: 8px; }
.grid-item { background: antiquewhite; border: 1px solid rgba(0,0,0,.8); padding: 4px; margin: 3px; font-size: 16px; text-align: center; }
</style>
<p style="margin:2px">A) grid-column: 1 / span 3 cho item 1</p>
<div class="grid-container" id="a">
<div class="grid-item" style="grid-column: 1 / span 3">1</div>
<div class="grid-item">2</div><div class="grid-item">3</div><div class="grid-item">4</div>
<div class="grid-item" style="grid-column: 1 / span 2">5</div><div class="grid-item">6</div>
</div>
<p style="margin:2px">B) grid-row: 2 / span 2 cho item 2</p>
<div class="grid-container" id="b">
<div class="grid-item" style="grid-column: 1 / span 3">1</div>
<div class="grid-item" style="grid-row: 2 / span 2">2</div>
<div class="grid-item">3</div><div class="grid-item">4</div><div class="grid-item">5</div><div class="grid-item">6</div>
</div>
<p style="margin:2px">C) grid-area: 1 / 2 / 4 / 3 cho item 7</p>
<div class="grid-container" id="c">
<div class="grid-item">1</div><div class="grid-item">2</div><div class="grid-item">3</div>
<div class="grid-item">4</div><div class="grid-item">5</div><div class="grid-item">6</div>
<div class="grid-item" style="grid-area: 1 / 2 / 4 / 3">7</div>
</div>
<script>
console.log("A item1 grid-column:", document.querySelector("#a .grid-item").style.gridColumn);
console.log("B item2 grid-row :", document.querySelectorAll("#b .grid-item")[1].style.gridRow);
console.log("C item7 grid-area :", document.querySelectorAll("#c .grid-item")[6].style.gridArea);
</script>Ngoài ra, ta có thể dùng kỹ thuật đặt tên cho các vùng để dựng bố cục. Đây là ví dụ cuối của giáo trình: sáu cột và ba hàng; vùng header chiếm cả hàng đầu, menu chiếm cột đầu của hai hàng sau, main chiếm ba cột, right hai cột và footer năm cột. Cách này dễ đọc nhất vì bạn "vẽ" bố cục ngay trong CSS.
<style>
.item1 { grid-area: header; }
.item2 { grid-area: menu; }
.item3 { grid-area: main; }
.item4 { grid-area: right; }
.item5 { grid-area: footer; }
.grid-container {
display: grid; background: beige;
grid-template-areas:
'header header header header header header'
'menu main main main right right'
'menu footer footer footer footer footer';
}
.grid-container > div { background: antiquewhite; border: 1px solid rgba(0,0,0,.8); padding: 10px; margin: 4px; font-size: 22px; text-align: center; }
</style>
<div class="grid-container" id="g">
<div class="item1">Header</div><div class="item2">Menu</div><div class="item3">Main</div>
<div class="item4">Right</div><div class="item5">Footer</div>
</div>
<script>
const g = document.getElementById("g");
for (const el of g.children) console.log(el.textContent.padEnd(7), "-> grid-area:", getComputedStyle(el).gridArea);
</script>Bố cục lưới (Grid): hàng, cột và vùng
đổi mẫu cột/hàng, kéo gap, bấm item để cho nó trải nhiều ô.container { display: grid; grid-template-columns: repeat(3, 1fr); grid-template-rows: repeat(2, 70px); gap: 10px; }
Phòng thí nghiệm Grid cho bạn đổi số cột, gap, đơn vị fr và đặt một ô qua nhiều cột/hàng; hãy thử tạo lại bố cục "Header – Menu – Main – Right – Footer" ở trên bằng cách kéo các ô. Giáo trình cũng giới thiệu công cụ "CSS Grid trực quan" cùng mục đích.
Vừa học xong Grid, hãy làm các bài sau để luyện: lưới nhiều cột, đặt tên vùng, ô lớn nổi bật trong thư viện ảnh và bố cục Bento.
3. Thiết kế bố cục cho Web
Giáo trình: Chương 4 — mục "Thiết kế bố cục cho Web", gồm: "Bố cục chung", "Thiết kế phần đầu (Header)", "Thiết kế thanh điều hướng (Nav)", "Thiết kế phần chính (Content)" và "Thiết kế phần chân (Footer)".
3.1. Bố cục chung
Một trang Web thường được chia thành các phần header, menu, content và footer; trong đó phần content có thể chứa thêm các aside (cột phụ). Có rất nhiều bố cục khác nhau để lựa chọn, tuy nhiên bố cục Web dùng 3 cột (cột trái, nội dung chính, cột phải) là một trong những bố cục phổ biến nhất.
Việc thiết kế bố cục Website có thể dùng flexbox bằng cách tạo một flex container bao quanh toàn bộ các phần, và bật flex-wrap: wrap để các phần có thể chiếm cả hàng hoặc chia hàng:
<div class="flex-container">
…
</div>.flex-container {
display: flex;
flex-wrap: wrap;
}Mẹo của giáo trình là cho các phần "toàn chiều ngang" (header, nav, footer) flex-basis: 100%. Vì basis 100% đã lấp đầy một hàng nên mỗi phần như vậy tự chiếm một hàng riêng; còn các phần có basis nhỏ hơn (aside, article) sẽ nằm chung hàng với nhau.
3.2. Thiết kế phần đầu (Header)
Phần đầu của Website thường nằm ở phía trên cùng của trang (hoặc ngay phía dưới menu điều hướng), thường chứa logo kèm tên Website. Quy tắc giáo trình: header { flex-basis: 100%; background-color: gainsboro; padding: 20px; margin: 5px; }.
3.3. Thiết kế thanh điều hướng (Nav)
Thanh điều hướng chứa một danh sách các liên kết để giúp người dùng đến các mục khác nhau của Website. Bản thân nav vừa là một phần tử của container lớn (có flex-basis: 100%) vừa là một flex container nhỏ (display: flex) để các liên kết a xếp thành hàng. Mỗi liên kết có padding: 20px, bỏ gạch chân bằng text-decoration: none, và đổi nền khi rê chuột bằng lớp giả :hover.
3.4. Thiết kế phần chính (Content)
Cách bố trí phần này phụ thuộc vào mục đích người dùng. Cách phổ biến nhất là dùng các cột, thường từ 1 đến 3 cột:
- 1 cột: thường dùng cho trình duyệt di động.
- 2 cột: thường dùng cho máy tính bảng và máy tính xách tay.
- 3 cột: chỉ dùng cho máy tính để bàn.
Các nhà thiết kế thường dựng bố cục 3 cột rồi chuyển thành bố cục 1 hoặc 2 cột trên màn hình nhỏ hơn bằng @media. Ví dụ giáo trình: aside.left với flex: 1 0, article với flex: 3 0 và aside.right với flex: 1 0 (tỷ lệ 1 : 3 : 1). Cột giữa nhận gấp ba phần không gian của mỗi cột bên.
3.5. Thiết kế phần chân (Footer)
Footer được đặt ở cuối trang Web, thường chứa thông tin như bản quyền và thông tin liên hệ: footer { background-color: gainsboro; flex-basis: 100%; padding: 20px; margin: 5px; }.
Ghép lại: bố cục Web hoàn chỉnh
Ghép tất cả các quy tắc trên, ta có trang dưới đây (chưa có phần đáp ứng, phần này sẽ thêm ở mục 4). Hãy chạy và quan sát: header, nav, footer chiếm cả hàng; ba cột nằm chung một hàng theo tỷ lệ 1 : 3 : 1. Console in ra tỷ lệ flex-grow đã đặt của từng phần.
<style>
body { font-family: Arial; margin: 0; }
.flex-container { display: flex; flex-wrap: wrap; }
header { flex-basis: 100%; background-color: gainsboro; padding: 20px; margin: 5px; }
nav { flex-basis: 100%; background-color: gainsboro; margin: 5px; display: flex; }
nav a { padding: 20px; overflow: auto; text-decoration: none; color: black; }
nav a:hover { background-color: #333; color: white; }
aside.left { background: #F1F1F1; flex: 1 0; padding: 20px; margin: 5px; }
article { text-align: left; background: #F1F1F1; flex: 3 0; padding: 20px 20px 100px; margin: 5px; }
aside.right { background: #F1F1F1; flex: 1 0; padding: 20px; margin: 5px; }
footer { background-color: gainsboro; flex-basis: 100%; padding: 20px; margin: 5px; }
</style>
<div class="flex-container">
<header>Header</header>
<nav><a href="#">Link</a><a href="#">Link</a><a href="#">Link</a></nav>
<aside class="left">Aside Left</aside>
<article>Main Content</article>
<aside class="right">Aside Right</aside>
<footer>Footer</footer>
</div>
<script>
for (const sel of ["aside.left", "article", "aside.right"]) {
const s = getComputedStyle(document.querySelector(sel));
console.log(sel.padEnd(11), "flex-grow =", s.flexGrow);
}
</script>Ví dụ thực tế: Trang chủ của hầu hết báo điện tử đều theo "khung xương" này: dải logo (header), menu chuyên mục (nav), cột tin chính ở giữa, cột phụ chứa tin nổi bật hoặc quảng cáo (aside) và chân trang. Khi bạn đã nắm khung xương, việc "nhìn" một trang web và đoán cách dựng nó sẽ trở nên dễ dàng.
Mở rộng: Cũng khung xương này có thể dựng bằng Grid chỉ với
grid-template-areas(xem phần thực hành Bài 4.4). Trong thực tế, Grid rất hợp để dựng khung trang tổng thể, Flexbox hợp để dựng các thành phần bên trong (nav, thẻ, thanh công cụ); hai kỹ thuật bổ sung cho nhau chứ không loại trừ nhau.
Vừa học xong cách dựng bố cục trang, hãy làm các bài sau: header – nội dung – chân trang, bố cục Holy Grail (hai sidebar kẹp nội dung) và chân trang luôn sát đáy.
4. Thiết kế đáp ứng
Giáo trình: Chương 4 — mục "Thiết kế đáp ứng", gồm: "Khái niệm về thiết kế đáp ứng", "Khai báo khung nhìn (viewport)", "Thiết lập điều kiện (Media queries)", "Thiết kế Di động trước (Mobile first)", "Thiết kế đáp ứng cho các nội dung đa phương tiện", "Thiết kế đáp ứng thanh điều hướng", "Ẩn các phần tử không cần thiết" và "Tùy chỉnh cỡ chữ".
4.1. Khái niệm về thiết kế đáp ứng
Thiết kế Web đáp ứng (responsive web design) làm cho trang Web hiển thị hợp lý, nhằm tăng trải nghiệm người dùng trên các thiết bị khác nhau: máy tính để bàn, máy tính bảng, điện thoại.
Thiết kế Web đáp ứng (RWD): một mã nguồn nhưng bố cục tự thích nghi với kích thước màn hình, thay vì làm riêng từng phiên bản.
Các trang Web có kích thước cố định thường quá lớn so với màn hình thiết bị di động. Để khắc phục, trình duyệt trên các thiết bị này tự động thu nhỏ toàn bộ trang cho vừa màn hình; khi đó chiều ngang quá nhỏ làm người dùng phải vuốt ngang để xem hết nội dung, hoặc xem trang với chữ quá nhỏ và phải zoom mới đọc được. Điều này khiến người dùng khó chịu khi lướt Web. Vì vậy nhà thiết kế cần thiết kế Web đáp ứng nhu cầu của người dùng.
Thiết kế Web đáp ứng dùng CSS và HTML, đôi khi kết hợp thêm JavaScript, để thay đổi kích thước, ẩn, thu nhỏ, phóng to hoặc di chuyển nội dung sao cho trang trông đẹp hơn trên bất kỳ màn hình nào.
4.2. Khai báo khung nhìn (viewport)
Trước khi viết bất kỳ điều kiện hiển thị nào, tài liệu HTML bắt buộc phải khai báo thẻ viewport trong phần head:
<meta name="viewport" content="width=device-width, initial-scale=1.0">Nếu thiếu thẻ này, trình duyệt trên điện thoại sẽ giả lập một khung nhìn rộng khoảng 980 pixel rồi thu nhỏ toàn trang cho vừa màn hình; khi đó mọi media query viết theo chiều rộng thiết bị đều không có tác dụng. Trong đó width=device-width đặt chiều rộng khung nhìn bằng đúng chiều rộng màn hình thiết bị, còn initial-scale=1.0 đặt tỷ lệ phóng ban đầu là 1:1.
Cảnh báo: Không thêm
user-scalable=nohaymaximum-scale=1.0. Những khai báo này chặn người dùng phóng to trang, vi phạm tiêu chuẩn về khả năng tiếp cận (accessibility): người kém mắt sẽ không thể đọc nội dung của bạn.
4.3. Thiết lập điều kiện (Media queries)
Media queries dùng để kiểm tra các điều kiện của thiết bị (đặc biệt là kích thước của viewport), từ đó áp dụng các CSS phù hợp. Cú pháp:
@media not|only mediatype and (expressions) {
CSS-Code;
}Ví dụ áp dụng cho CSS cụ thể: nếu chiều rộng nằm giữa 600px và 1000px thì đổi màu nền.
@media only screen and (max-width: 1000px) and (min-width: 600px) {
body { background-color: lightblue; }
}Có thể áp dụng cho cả một tệp CSS riêng: thẻ link có thuộc tính media, ví dụ <link rel="stylesheet" media="screen and (min-width: 800px)" href="rong.css">; tệp chỉ được áp dụng khi điều kiện đúng.
Mediatype giới hạn loại phương tiện mà CSS áp dụng:
| Mediatype | Dùng cho |
|---|---|
all |
Mọi loại thiết bị |
print |
Máy in (khi in trang) |
screen |
Màn hình máy tính, máy tính bảng, điện thoại thông minh |
speech |
Các trình đọc màn hình |
Biểu thức điều kiện (expression) gồm các đặc tính phương tiện (media feature). Nhóm hay dùng nhất:
| Đặc tính | Ý nghĩa |
|---|---|
width, min-width, max-width |
Chiều rộng viewport (tối thiểu / tối đa) |
height, min-height, max-height |
Chiều cao viewport |
aspect-ratio, min-aspect-ratio, max-aspect-ratio |
Tỉ lệ rộng : cao của viewport |
orientation |
Hướng khung nhìn (portrait dọc / landscape ngang) |
resolution |
Độ phân giải của thiết bị xuất (dpi hoặc đơn vị tương tự) |
color, color-index |
Số bit cho mỗi màu / số màu thiết bị hiển thị được |
Lưu ý: Giáo trình nhắc rằng nhóm đặc tính có tiền tố
device-(device-width,device-height,device-aspect-ratiocùng các biến thểmin-,max-) đã bị loại khỏi tiêu chuẩn và không nên dùng cho mã mới. Hãy dùngwidth,heightvàaspect-ratiocủa khung nhìn.
Ví dụ: nếu chiều rộng trình duyệt nhỏ hơn hoặc bằng 600px thì bỏ margin quanh body:
@media only screen and (max-width: 600px) {
body { margin: 0; }
}/* Mặc định = di động, viết trước */ .main { grid-template-columns: 1fr; }
@media (min-width: 600px) { .main { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 800px) { .main { grid-template-columns: repeat(3, 1fr); } .aside { display: block; } }
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- bắt buộc, thiếu thì media query không có tác dụng trên điện thoại -->
Viết CSS cho điện thoại trước, rồi dùng @media (min-width) để bổ sung khi màn hình rộng hơn; mỗi điểm ngắt cộng thêm quy tắc lên trên quy tắc cũ.
Hình trên minh họa các điểm ngắt (breakpoint): những chiều rộng mà tại đó bố cục "gãy" sang một dạng khác. Giáo trình dùng hai điểm ngắt 800px và 600px; trong thực tế bạn chọn điểm ngắt theo chỗ nội dung bắt đầu xấu đi, không phải theo tên thiết bị cụ thể.
Để kiểm tra bạn hiểu cách trình duyệt "chọn" quy tắc, đoạn mã sau tự viết một bộ đánh giá điều kiện min-width/max-width rồi thử với vài chiều rộng. Chú ý: ở chiều rộng 800px, điều kiện max-width: 800px vẫn đúng (giá trị biên được tính vào).
// Bộ đánh giá media query đơn giản: hỗ trợ "(min-width: Npx)", "(max-width: Npx)" nối bằng "and"
function khop(query, width) {
const dieuKien = [...query.matchAll(/\(\s*(min|max)-width\s*:\s*(\d+)px\s*\)/g)];
return dieuKien.every(([, loai, n]) => (loai === "min" ? width >= +n : width <= +n));
}
const rules = [
"(max-width: 600px)",
"(max-width: 800px)",
"(max-width: 1000px) and (min-width: 600px)",
"(min-width: 800px)",
];
for (const w of [360, 600, 700, 800, 1024]) {
const dung = rules.filter((r) => khop(r, w));
console.log(`${String(w).padStart(4)}px -> áp dụng ${dung.length} khối:`, dung.join(" ; "));
}Bây giờ áp dụng vào bố cục ở mục 3: chỉnh số cột theo kích thước. Với màn hình tối đa 800px, article chiếm cả hàng (flex-basis: 100%) và đứng đầu (order: 1), hai aside xuống hàng dưới (order: 2 và 3), footer cuối cùng (order: 4). Với màn hình tối đa 600px, thanh nav chuyển thành cột (flex-direction: column) và cả hai aside cũng chiếm cả hàng. Đây là mã của giáo trình:
@media (max-width: 800px) {
aside.left { order: 2; }
article { order: 1; flex-basis: 100%; }
aside.right { order: 3; }
footer { order: 4; }
}
@media (max-width: 600px) {
nav { flex-direction: column; }
aside.left { order: 2; flex-basis: 100%; }
article { order: 1; flex-basis: 100%; }
aside.right { order: 3; flex-basis: 100%; }
footer { order: 4; }
}Bản chạy thử dưới đây gắn phần đáp ứng đó vào bố cục ba cột. Để thấy hiệu ứng, bạn hãy thu hẹp cửa sổ trình duyệt (hoặc dùng demo responsive-lab bên dưới). Phần console liệt kê các điểm ngắt có trong bảng định kiểu bằng cách đọc document.styleSheets — kết quả không phụ thuộc vào kích thước cửa sổ.
<style>
body { font-family: Arial; margin: 0; }
.flex-container { display: flex; flex-wrap: wrap; }
header, footer { flex-basis: 100%; background: gainsboro; padding: 14px; margin: 5px; }
nav { flex-basis: 100%; background: gainsboro; margin: 5px; display: flex; }
nav a { padding: 14px; text-decoration: none; color: black; }
aside.left, aside.right { background: #F1F1F1; flex: 1 0; padding: 14px; margin: 5px; }
article { background: #cfe9ff; flex: 3 0; padding: 14px 14px 50px; margin: 5px; }
@media (max-width: 800px) {
aside.left { order: 2; } article { order: 1; flex-basis: 100%; }
aside.right { order: 3; } footer { order: 4; }
}
@media (max-width: 600px) {
nav { flex-direction: column; }
aside.left { order: 2; flex-basis: 100%; } article { order: 1; flex-basis: 100%; }
aside.right { order: 3; flex-basis: 100%; } footer { order: 4; }
}
</style>
<div class="flex-container">
<header>Header</header>
<nav><a href="#">Trang chủ</a><a href="#">Bài viết</a><a href="#">Liên hệ</a></nav>
<aside class="left">Aside Left</aside>
<article>Main Content</article>
<aside class="right">Aside Right</aside>
<footer>Footer</footer>
</div>
<script>
const cacDiemNgat = [...document.styleSheets[0].cssRules]
.filter((r) => r.media)
.map((r) => r.media.mediaText.replace(/\s+/g, ""));
console.log("Số khối @media:", cacDiemNgat.length);
console.log("Các điều kiện:", cacDiemNgat.join(" | "));
</script>Thiết kế đáp ứng: kéo độ rộng khung nhìn
kéo thanh trượt, xem điểm ngắt đổi bố cục.flex-container { display: flex; flex-wrap: wrap; } aside { flex: 0 0 20%; } article { flex: 1; }
@media (max-width: 800px) { article { order: 1; flex-basis: 100%; } aside.left { order: 2; flex: 1 1 50%; } aside.right { order: 3; flex: 1 1 50%; } footer { order: 4; } }
@media (max-width: 600px) { nav { flex-direction: column; } aside { flex-basis: 100%; } }
Hai cách viết cho kết quả như nhau: Mobile first viết kiểu mặc định cho điện thoại rồi mở rộng dần bằng min-width.
Trong phòng thí nghiệm responsive-lab, hãy kéo độ rộng của khung và quan sát bố cục đổi dạng tại các điểm ngắt; thử đoán trước rồi mới kéo. Giáo trình cũng kèm công cụ "Responsive và media query" để thực nghiệm.
4.4. Thiết kế "Di động trước" (Mobile first)
Mobile first là cách thiết kế cho "di động trước", rồi mới áp dụng các media query cho các thiết bị có kích thước lớn. Đây là cách thiết kế phổ biến vì giúp tốc độ hiển thị trên thiết bị di động nhanh hơn: điện thoại chỉ cần tải và xử lý mã cơ bản, không phải "gỡ" các quy tắc của máy tính để bàn.
Nguyên tắc: viết trước bố cục cho màn hình nhỏ (một cột), sau đó dùng @media (min-width: …) để bổ sung bố cục cho màn hình lớn. Trong ví dụ giáo trình, các phần tử header, nav, aside.left, article, aside.right, footer đều có flex-basis: 100% nên trên điện thoại chúng xếp chồng nhau và chiếm toàn bộ chiều rộng. Khi màn hình đạt tối thiểu 800px, một media query điều chỉnh các phần tử aside.left, article, aside.right để phân bố đều trong container; article được đặt flex-grow lớn hơn nên chiếm nhiều không gian hơn hai cột bên.
| Cách tiếp cận | Quy tắc gốc viết cho | Media query dùng | Ưu điểm |
|---|---|---|---|
| Desktop first | Màn hình lớn | max-width (thu nhỏ dần) |
Quen thuộc với dự án có sẵn giao diện máy tính |
| Mobile first | Màn hình nhỏ | min-width (mở rộng dần) |
Mã gọn, tải nhanh trên di động |
<style>
body { font-family: Arial; margin: 0; }
/* Mặc định (di động): mọi phần tử chiếm 100% -> xếp chồng */
.flex-container { display: flex; flex-wrap: wrap; }
header, nav, aside.left, article, aside.right, footer { flex-basis: 100%; margin: 5px; padding: 12px; background: gainsboro; }
article { background: #cfe9ff; }
aside.left { order: 2; }
/* Từ 800px trở lên: chia cột */
@media (min-width: 800px) {
aside.left { flex: 1 0 0; order: 0; }
article { flex: 3 0 0; }
aside.right { flex: 1 0 0; }
}
</style>
<div class="flex-container">
<header>Header</header><nav>Nav</nav>
<aside class="left">Aside Left</aside><article>Article (grow lớn nhất)</article><aside class="right">Aside Right</aside>
<footer>Footer</footer>
</div>
<script>
const rules = [...document.styleSheets[0].cssRules];
console.log("Quy tắc gốc (di động):", rules.filter((r) => !r.media).length, "| khối @media:", rules.filter((r) => r.media).length);
console.log("Điều kiện mở rộng:", rules.filter((r) => r.media).map((r) => r.media.mediaText.replace(/\s+/g, "")).join(","));
</script>4.5. Thiết kế đáp ứng cho các nội dung đa phương tiện
Khi hiển thị ảnh nội dung trên điện thoại, nên để ảnh co giãn theo chiều rộng của khối chứa để ảnh luôn vừa vặn: article img { width: 100%; }. Khi hiển thị trên máy tính, ảnh chỉ cần chiếm một phần màn hình, phần còn lại hiển thị chữ; dùng media query cùng float:
@media (min-width: 800px) {
article img {
width: 240px;
margin-right: 20px;
margin-bottom: 10px;
float: left;
}
}Kết quả: ảnh trôi về bên trái, phần chữ chảy quanh ảnh. Khi cần dùng nhiều ảnh khác nhau cho các kích thước màn hình khác nhau, dùng phần tử picture kết hợp với source và img: tùy kích thước màn hình mà trình duyệt chọn bức ảnh phù hợp.
<picture>
<source media="(min-width:650px)" srcset="flower1.jpg">
<source media="(min-width:465px)" srcset="flower2.jpg">
<img src="flower3.jpg" alt="Flowers" style="width:auto;">
</picture>Trình duyệt duyệt các source từ trên xuống và chọn source đầu tiên có điều kiện media đúng; nếu không có cái nào đúng, nó dùng img làm ảnh mặc định (và img luôn bắt buộc có mặt). Vì vậy phải xếp điều kiện rộng nhất lên trước. Đoạn mã dưới mô phỏng quy tắc chọn ảnh.
// Mô phỏng cách trình duyệt chọn ảnh trong <picture>
const sources = [
{ min: 650, file: "flower1.jpg" },
{ min: 465, file: "flower2.jpg" },
];
const macDinh = "flower3.jpg";
function chonAnh(width) {
const s = sources.find((x) => width >= x.min); // source đầu tiên khớp
return s ? s.file : macDinh;
}
for (const w of [320, 465, 500, 649, 650, 1200]) console.log(String(w).padStart(5) + "px ->", chonAnh(w));Ví dụ trực quan: ảnh co theo khối chứa, và sang bố cục máy tính thì trôi trái. (Ảnh minh họa dùng SVG nhúng sẵn để không cần mạng.)
<style>
body { font-family: Arial; margin: 6px; }
article { background: #f6f6f6; padding: 10px; overflow: hidden; }
article img { width: 100%; height: auto; display: block; }
@media (min-width: 800px) {
article img { width: 240px; margin-right: 20px; margin-bottom: 10px; float: left; }
}
</style>
<article>
<img id="anh" alt="Ảnh minh họa" src="data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='480' height='240'%3E%3Crect width='480' height='240' fill='%231abc9c'/%3E%3Ccircle cx='240' cy='120' r='70' fill='white'/%3E%3C/svg%3E">
<p>Trên điện thoại ảnh chiếm trọn chiều rộng khối chứa; từ 800px trở lên ảnh chỉ rộng 240px và trôi về bên trái để chữ chảy quanh ảnh. Hãy kéo rộng khung để thấy ảnh đổi vị trí.</p>
</article>
<script>
console.log("Thẻ ảnh:", document.getElementById("anh").alt);
console.log("Số quy tắc media:", [...document.styleSheets[0].cssRules].filter((r) => r.media).length);
</script>4.6. Thiết kế đáp ứng thanh điều hướng
Trên điện thoại, menu thường có kích thước lớn để người dùng dễ thao tác (xếp cột, chữ căn giữa); máy tính có màn hình lớn nên menu thường hiển thị theo hàng.
nav {
flex-basis: 100%;
background-color: gainsboro;
margin: 5px;
display: flex;
flex-direction: column;
text-align: center;
}
@media (min-width: 800px) {
nav { flex-direction: row; }
}Có thể tạo menu Hamburger chỉ bằng HTML/CSS với hộp kiểm (checkbox): một label (biểu tượng ☰, mã ☰) gắn với input type="checkbox" ẩn; khi hộp kiểm được chọn, bộ chọn input:checked ~ nav hiển thị menu. Với màn hình lớn (từ 800px), ẩn nút Hamburger đi và hiển thị các liên kết như bình thường. Hãy bấm vào ☰ trong ví dụ và quan sát (kéo rộng khung ≥ 800px để thấy menu hiện đủ, nút ☰ biến mất).
<style>
body { font-family: Arial; margin: 0; }
nav { background-color: gainsboro; margin: 5px; display: flex; flex-direction: column; text-align: center; display: none; }
nav a { padding: 12px; text-decoration: none; color: black; }
nav a:hover { background: #333; color: white; }
#hamburger-label { display: block; width: 100%; font-size: 1.2em; padding: 10px; margin: 5px; background-color: gainsboro; text-align: center; cursor: pointer; box-sizing: border-box; }
input:checked ~ nav { display: flex; flex-direction: column; }
#hamburger { display: none; }
@media (min-width: 800px) {
nav { flex-direction: row; display: flex; }
#hamburger-label { display: none; }
}
</style>
<label for="hamburger" id="hamburger-label">☰</label>
<input type="checkbox" id="hamburger"/>
<nav><a href="#">Trang chủ</a><a href="#">Bài viết</a><a href="#">Liên hệ</a></nav>
<script>
const cb = document.getElementById("hamburger");
console.log("Menu mở?", cb.checked);
cb.addEventListener("change", () => console.log("Menu mở?", cb.checked));
cb.checked = true; cb.dispatchEvent(new Event("change"));
cb.checked = false; cb.dispatchEvent(new Event("change"));
</script>Mẹo: Menu Hamburger bằng checkbox chạy không cần JavaScript, nhưng cần bổ sung thuộc tính trợ năng (ví dụ
aria-label, và cho phép điều khiển bằng bàn phím) nếu bạn dùng cho Website thật. Nhiều nhóm vẫn chọn JavaScript để bật/tắt một lớp CSS cho linh hoạt.
4.7. Ẩn các phần tử không cần thiết
Trong một số trường hợp, nhà thiết kế có thể phải ẩn một số thành phần không cần thiết trên các thiết bị màn hình nhỏ để nhường chỗ cho nội dung quan trọng. Ví dụ giáo trình:
@media screen and (max-width: 600px) {
header.logo { display: none; }
}Lưu ý:
display: noneloại phần tử khỏi bố cục (và khỏi trình đọc màn hình). Chỉ ẩn những gì thật sự không cần trên di động; đừng giấu chức năng quan trọng (như tìm kiếm) chỉ vì màn hình nhỏ — hãy thu gọn nó lại.
4.8. Tùy chỉnh cỡ chữ
Nhà thiết kế có thể đổi cỡ chữ cho phù hợp với từng thiết bị: ví dụ header có font-size: 80px khi màn hình rộng từ 600px và font-size: 30px khi hẹp hơn 600px. Ngoài ra có thể dùng đơn vị tương đối (em, rem, %, vw) để cỡ chữ tự co theo ngữ cảnh mà không cần viết nhiều media query.
<style>
body { font-family: Arial; margin: 0; }
header { background: #1abc9c; color: white; text-align: center; padding: 10px; font-size: 30px; }
@media screen and (min-width: 600px) { header { font-size: 60px; } }
p.tuongdoi { font-size: 1.25rem; padding: 0 10px; }
</style>
<header id="h">Thiết kế Web</header>
<p class="tuongdoi">Đoạn này dùng rem nên co giãn theo cỡ chữ gốc của trình duyệt.</p>
<script>
const kq = [...document.styleSheets[0].cssRules].filter((r) => r.media).map((r) => r.media.mediaText.replace(/\s+/g, ""));
console.log("Điểm ngắt cỡ chữ:", kq.join(","));
console.log("font-size của p.tuongdoi (khai báo):", "1.25rem");
</script>Vừa học xong thiết kế đáp ứng, hãy làm các bài sau để luyện: lưới tự đổi số cột bằng auto-fit, ba mức 4 → 2 → 1 cột, thanh điều hướng đáp ứng và bố cục tạp chí nhiều mức.
Thực hành theo giáo trình
Giáo trình: Chương 4 — mục "Thực hành" gồm Bài 4.1 (Thiết kế Website 1), Bài 4.2 (Thiết kế Website 2), Bài 4.3 (Thiết kế Website MinhBlogs) và mục "Bài tập tự thực hành" (Bài 4.4). Các bài này dựng dần từ bố cục flex đơn giản đến một Website nhiều trang, đáp ứng.
Bài 4.1. Thiết kế Website 1
Mục tiêu: dựng bố cục gồm header, aside 1, article, aside 2, footer bằng Flexbox và làm đáp ứng cho màn hình nhỏ hơn 800px và 600px. Các bước theo giáo trình:
- Tạo bố cục ban đầu: một
div.flex-containerchứaheader,aside.aside1,article(văn bản Lorem ipsum),aside.aside2,footer. Chạy thử để thấy các khối xếp chồng. - Thêm định dạng màu sắc và flex-container:
display: flex; flex-flow: row wrap; text-align: center;, mỗi con trực tiếp cópadding: 10px(bộ chọn.flex-container > *); header màucoral, aside1moccasin, articlecornflowerblue(căn trái), aside2violet, footerlightgreen. - Thêm thuộc tính
flexcho các khối: headerflex: 1 100%, aside1flex: 1 0px, articleflex: 3 0px, aside2flex: 1 0px, footerflex: 1 100%. - Đáp ứng cho màn hình nhỏ hơn 800px:
articleđứng đầu và chiếm cả hàng (order: 1; flex: 1 100%), aside1order: 2, aside2order: 3, footerorder: 4. - Đáp ứng cho màn hình nhỏ hơn 600px: cả hai aside và article đều
flex: 1 100%, xếp thành một cột.
Bản dưới ghép đủ các bước. Hãy kéo thu hẹp khung xem trước và quan sát bố cục đổi theo ba dạng (rộng, trung bình, điện thoại).
<style>
.flex-container { display: flex; flex-flow: row wrap; text-align: center; }
.flex-container > * { padding: 10px; }
header { background: coral; flex: 1 100%; }
.aside1 { background: moccasin; flex: 1 0px; }
article { text-align: left; background: cornflowerblue; flex: 3 0px; }
.aside2 { background: violet; flex: 1 0px; }
footer { background: lightgreen; flex: 1 100%; }
@media (max-width: 800px) {
.aside1 { order: 2; flex: 1 0px; }
article { order: 1; flex: 1 100%; }
.aside2 { order: 3; flex: 1 0px; }
footer { order: 4; }
}
@media (max-width: 600px) {
.aside1 { order: 2; flex: 1 100%; }
article { order: 1; flex: 1 100%; }
.aside2 { order: 3; flex: 1 100%; }
}
</style>
<div class="flex-container">
<header>Header</header>
<aside class="aside1">Aside 1</aside>
<article>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque sed ex turpis. Cras luctus nibh lectus, in ullamcorper ex tempor eleifend. Nulla bibendum, eros a consequat vestibulum.</article>
<aside class="aside2">Aside 2</aside>
<footer>Footer</footer>
</div>
<script>
const khoi = [...document.querySelector(".flex-container").children].map((e) => e.tagName.toLowerCase() + (e.className ? "." + e.className : ""));
console.log("Các khối theo thứ tự HTML:", khoi.join(" > "));
console.log("Số khối @media:", [...document.styleSheets[0].cssRules].filter((r) => r.media).length);
</script>Bài 4.2. Thiết kế Website 2
Mục tiêu: sửa Website 1 thành trang 2 cột có header, navbar, blog bên phải và aside bên trái. Các bước giáo trình:
- Xóa
aside2 để chỉ còn 2 cột; sửaheadercóh1"My Website" và đoạn "With a flexible layout.";footercóh2"Footer". - Sửa định dạng header (
flex: 1 100%; background: #1abc9c; color: white; padding: 60px; text-align: center) và footer (flex: 100%; padding: 20px; text-align: center; background: #ddd); đổi font và margin:body { font-family: Arial; margin: 0; }. - Tạo Navbar sau header:
div.navbarvới bốn liên kết,display: flex; background-color: #333; flex: 100%; padding: 0;; liên kếtpadding: 14px 20px, chữ trắng, bỏ gạch chân,:hoverđổi nền#dddchữ đen. - Sửa aside (
flex: 30%, nền#f1f1f1) và article (flex: 70%, nền trắng). Nếu hai cột bị "nhảy xuống dòng" vì cộngpaddingvượt 100%, thêmbox-sizing: border-box(đây chính là lỗi của mô hình hộp ở buổi 8). - Bổ sung nội dung aside ("About Me", ảnh, đoạn giới thiệu) và article (hai bài với
div.fakeimgcao 200px, nền#aaa). - Đáp ứng cho màn hình nhỏ hơn 800px:
.flex-container, .navbar { flex-direction: column; }.
<style>
body { font-family: Arial; margin: 0; }
.flex-container { display: flex; flex-flow: row wrap; text-align: center; }
.flex-container > * { padding: 10px; box-sizing: border-box; }
header { flex: 1 100%; background: #1abc9c; color: white; padding: 30px; text-align: center; }
header h1 { margin: 0; }
.navbar { display: flex; background-color: #333; flex: 100%; padding: 0px; }
.navbar a { color: white; padding: 14px 20px; text-decoration: none; text-align: center; }
.navbar a:hover { background-color: #ddd; color: black; }
aside { flex: 30%; background-color: #f1f1f1; text-align: center; }
article { flex: 70%; background-color: white; text-align: left; }
.fakeimg { background-color: #aaa; width: 100%; height: 60px; }
footer { flex: 100%; padding: 20px; text-align: center; background: #ddd; }
@media screen and (max-width: 800px) { .flex-container, .navbar { flex-direction: column; } }
</style>
<div class="flex-container">
<header><h1>My Website</h1><p>With a <b>flexible</b> layout.</p></header>
<div class="navbar"><a href="#">Link</a><a href="#">Link</a><a href="#">Link</a><a href="#">Link</a></div>
<aside class="aside1"><h2>About Me</h2><p>Some text about me in culpa qui officia deserunt mollit anim..</p></aside>
<article>
<h2>TITLE HEADING</h2><h5>Title description, Dec 7, 2017</h5><div class="fakeimg"></div><p>Some text..</p>
<h2>TITLE HEADING</h2><h5>Title description, Sep 2, 2017</h5><div class="fakeimg"></div><p>Sunt in culpa qui officia deserunt.</p>
</article>
<footer><h2>Footer</h2></footer>
</div>
<script>
const aside = getComputedStyle(document.querySelector("aside"));
const art = getComputedStyle(document.querySelector("article"));
console.log("aside flex-grow/basis:", aside.flexGrow, aside.flexBasis);
console.log("article flex-grow/basis:", art.flexGrow, art.flexBasis);
console.log("box-sizing của aside:", aside.boxSizing);
</script>Lưu ý: Hai giá trị
flex: 30%vàflex: 70%có tổng 100%, nhưng nếu mỗi cột còn cópaddingmà không cóbox-sizing: border-boxthì tổng chiều rộng thực vượt 100% và cột thứ hai bị đẩy xuống dòng. Đây là lý do giáo trình hướng dẫn "sửa lỗi nhảy xuống" bằngbox-sizing: border-box.
Bài 4.3. Thiết kế Website MinhBlogs
Mục tiêu: dựng một Website hai trang (index.html và register.html) có tổ chức thư mục chuyên nghiệp và thiết kế đáp ứng. Yêu cầu của giáo trình: (1) dùng các ảnh phù hợp với Website; (2) thiết kế đáp ứng với màn hình nhỏ hơn 800px; (3) lưu các tệp trong thư mục public, gồm index.html, thư mục css chứa style.css, thư mục js chứa main.js và thư mục images chứa các ảnh cần thiết.
public/
├── index.html
├── register.html
├── css/style.css
├── js/main.js
└── images/ (ảnh, favicon, svg)Các bước chính, theo đúng thứ tự giáo trình:
- Tạo
index.htmlvớidiv.flex-containerchứaheader.feature1,nav,section,main,footer; gắncss/style.cssbằng thẻlink. - Tạo
style.css: khai báo biến màu--primary-color: #1abc9cvà--primary-dark-color: #00a886trong:root;.flex-container { display: flex; flex-flow: row wrap; }; header chiếm 100%, nền ảnhbackground-size: cover;navflex nền#333;sectionflex: 30%,mainflex: 70%,footerflex: 100%. - Dựng
nav: liên kết Trang chủ, Bài viết, Dự án, Đăng ký khóa học, vàdiv.searchcóflex-grow: 1; display: flex; justify-content: flex-endđể ô tìm kiếm dạt về bên phải. - Dựng
section(thông tin cá nhân: ảnh đại diện bo trònborder-radius: 50%, trích dẫn, liên hệ) vàmain(ba bài viếtarticle; mỗi bài là một cột flex, nội dung.contentlà hàng flex có ảnh 200px, nút "Đọc thêm" dùngalign-self: flex-end). - Dựng
footer:footer-maindùngjustify-content: space-around; align-items: center, có biểu mẫu nhận email; dònglegalbản quyền. - Thêm khối gọi điện (
space1,space2,div.addùngposition: absolute) giữamainvàfooter. - Tạo
register.htmlvới headerfeature2, khối bốn khóa học (div.feature,justify-content: space-around), và biểu mẫu đăng ký gồm cácdiv.form-subblock(flex), ô họ tên, email, điện thoại, giới tính, khóa học, ngày bắt đầu, yêu cầu khác, nút "Nhập lại" và "Đăng ký". - Thiết kế đáp ứng (
@media screen and (max-width: 800px)):.flex-container, .footer-main, nav { flex-direction: column }, ẩn.space1, .space2, đưasection/div.featurelên trước bằngorder, và tự bổ sung CSS đáp ứng cho phần biểu mẫu (yêu cầu cuối của giáo trình).
Bản rút gọn dưới đây gom các kỹ thuật chính của bài: biến CSS, nav có ô tìm kiếm đẩy phải, section + main hai cột, thẻ bài viết dạng flex và đáp ứng dưới 800px.
<style>
:root { --primary-color: #1abc9c; --primary-dark-color: #00a886; }
body { font-family: Arial; margin: 0; }
.flex-container { display: flex; flex-flow: row wrap; }
.flex-container > * { box-sizing: border-box; }
header { flex-grow: 1; flex-basis: 100%; color: #fff; text-align: center; background: var(--primary-color); padding: 24px; }
header h1 { margin: 0; }
nav { display: flex; background-color: #333; flex: 100%; padding: 0; }
nav a { color: #fff; padding: 12px 18px; text-decoration: none; }
nav a:hover { background-color: #ddd; color: #000; }
nav div.search { flex-grow: 1; display: flex; justify-content: flex-end; padding: 5px; }
section { flex: 30%; background-color: #f1f1f1; text-align: center; padding: 10px; }
#avatar { width: 80px; height: 80px; border-radius: 50%; background: var(--primary-dark-color); display: inline-block; }
main { flex: 70%; background-color: #fff; display: flex; flex-direction: column; padding: 14px; }
article { display: flex; flex-direction: column; border-top: 1px solid rgba(26,188,156,.5); padding: 6px 0; }
article .content { display: flex; }
article .content .thumb { width: 90px; height: 60px; background: #ccc; border-radius: 4px; flex-shrink: 0; }
article .content div + div { padding-left: 10px; }
.readmore { align-self: flex-end; background-color: var(--primary-color); border-radius: 5px; color: #fff; padding: 4px 14px; text-decoration: none; }
footer { flex: 100%; text-align: center; background: #555; color: #fff; padding: 12px; }
@media screen and (max-width: 800px) {
.flex-container, nav { flex-direction: column; }
nav div.search { justify-content: stretch; }
nav div.search input { flex-grow: 1; }
section { order: 1; padding: 20px; }
footer { order: 2; }
}
</style>
<div class="flex-container">
<header><h1>Minh's Page</h1><p>Lập kế hoạch - Ghi chú thích - <b>Viết mã lệnh</b> - Kiểm thử</p></header>
<nav><a href="#">Trang chủ</a><a href="#">Bài viết</a><a href="#">Dự án</a><div class="search"><input type="search" placeholder="Tìm..."></div></nav>
<section><h3>Phạm Dương Minh</h3><span id="avatar"></span><p><q>Nếu không có đam mê, sẽ không có năng lượng</q></p></section>
<main>
<h2>BÀI VIẾT</h2>
<article><h3>Ngôn ngữ đánh dấu HTML</h3><div class="content"><div class="thumb"></div><div>HTML là ngôn ngữ đánh dấu dùng để xây dựng trang Web.</div></div><a class="readmore" href="#">Đọc thêm</a></article>
<article><h3>Bảng thuộc tính CSS</h3><div class="content"><div class="thumb"></div><div>Bảng tóm tắt các thuộc tính CSS để tra cứu nhanh.</div></div><a class="readmore" href="#">Đọc thêm</a></article>
</main>
<footer>© 2022 Bản quyền thuộc về Minh's Blogs</footer>
</div>
<script>
const root = getComputedStyle(document.documentElement);
console.log("--primary-color :", root.getPropertyValue("--primary-color").trim());
console.log("--primary-dark-color :", root.getPropertyValue("--primary-dark-color").trim());
console.log("Số bài viết:", document.querySelectorAll("main article").length);
console.log("Điểm ngắt:", [...document.styleSheets[0].cssRules].filter((r) => r.media).map((r) => r.media.mediaText.replace(/\s+/g, "")).join(","));
</script>Mẹo: Khi dựng một Website nhiều trang, hãy giữ phần chung (header, nav, footer, biến màu) trong một
style.cssduy nhất và dùng biến CSSvar(--primary-color); đổi màu chủ đạo của cả Website chỉ cần sửa một chỗ trong:root.
Bài 4.4. Thiết kế biểu mẫu đáp ứng (bài tập tự thực hành)
Giáo trình giao hai việc tự làm:
- Biểu mẫu đáp ứng: khi màn hình có độ rộng nhỏ hơn 600px, hãy làm cho hai cột của biểu mẫu xếp chồng lên nhau thay vì cạnh nhau.
- Làm lại bằng Grid: thiết kế lại hai trang Website 1 và Website 2 ở phần Thực hành bằng cách dùng Grid thay cho Flexbox.
Gợi ý cho việc 1: đặt hai cột vào một container display: flex (hoặc display: grid; grid-template-columns: 1fr 1fr), rồi trong @media (max-width: 600px) đổi thành flex-direction: column (hoặc grid-template-columns: 1fr). Gợi ý cho việc 2: Website 1 dựng bằng grid-template-areas, như bản dưới. Hãy so sánh với bản Flexbox ở Bài 4.1: cùng kết quả nhưng khung trang được "vẽ" thẳng bằng chữ.
<style>
.grid-container { display: grid; text-align: center;
grid-template-columns: 1fr 3fr 1fr;
grid-template-areas: 'header header header' 'aside1 article aside2' 'footer footer footer'; }
.grid-container > * { padding: 10px; }
header { grid-area: header; background: coral; }
.aside1 { grid-area: aside1; background: moccasin; }
article { grid-area: article; text-align: left; background: cornflowerblue; }
.aside2 { grid-area: aside2; background: violet; }
footer { grid-area: footer; background: lightgreen; }
@media (max-width: 800px) {
.grid-container { grid-template-columns: 1fr 1fr;
grid-template-areas: 'header header' 'article article' 'aside1 aside2' 'footer footer'; }
}
@media (max-width: 600px) {
.grid-container { grid-template-columns: 1fr;
grid-template-areas: 'header' 'article' 'aside1' 'aside2' 'footer'; }
}
</style>
<div class="grid-container" id="g">
<header>Header</header>
<aside class="aside1">Aside 1</aside>
<article>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque sed ex turpis. Cras luctus nibh lectus, in ullamcorper ex tempor eleifend.</article>
<aside class="aside2">Aside 2</aside>
<footer>Footer</footer>
</div>
<script>
console.log("display:", getComputedStyle(document.getElementById("g")).display);
console.log("Số khối @media:", [...document.styleSheets[0].cssRules].filter((r) => r.media).length);
console.log("Số vùng đặt tên:", ["header", "aside1", "article", "aside2", "footer"].length);
</script>Câu hỏi ôn tập
Giáo trình: Chương 4 — mục "Câu hỏi ôn tập lý thuyết". Các câu 4.1–4.4 (width, margin, đơn vị tương đối,
vh) thuộc nội dung buổi 8; buổi này ôn các câu 4.5–4.13 về Flexbox, Grid và bố cục đáp ứng. Đáp án đối chiếu Phụ lục.
Câu 4.5. Khi sử dụng flexbox, các phần tử con sẽ được sắp xếp như thế nào theo mặc định? (A. Theo chiều dọc; B. Theo chiều ngang; C. Các phần tử con không được sắp xếp theo thứ tự mặc định; D. Không có đáp án nào chính xác)
Đáp án: B — mặc định flex-direction: row, nên các phần tử xếp theo chiều ngang.
Câu 4.6. Thuộc tính flex-wrap được sử dụng để làm gì? (A. Điều chỉnh chiều cao của phần tử; B. Điều chỉnh chiều rộng của phần tử; C. Điều chỉnh hướng sắp xếp của các phần tử con; D. Điều chỉnh các phần tử con được xuống dòng, hoặc sang cột mới hay không)
Đáp án: D — flex-wrap quyết định các item có được xuống dòng/cột mới khi hết chỗ hay không. Hướng sắp xếp (đáp án C) là của flex-direction.
Câu 4.7. Thuộc tính flex được sử dụng để làm gì? (A. Điều chỉnh chiều cao; B. Điều chỉnh chiều rộng; C. Điều chỉnh hướng sắp xếp các phần tử con; D. Kết hợp các thuộc tính flex-grow, flex-shrink và flex-basis thành một thuộc tính duy nhất)
Đáp án: D — flex là thuộc tính viết tắt của ba thuộc tính trên, theo đúng thứ tự grow, shrink, basis.
Câu 4.8. Grid là gì trong CSS? (A. Một kiểu kích thước cho các phần tử HTML; B. Một phương pháp sắp xếp và bố trí các phần tử HTML; C. Một kiểu font chữ; D. Một kiểu màu nền)
Đáp án: B — Grid là phương pháp bố trí phần tử theo lưới gồm các hàng và cột.
Câu 4.9. Thuộc tính nào được dùng trong grid để chỉ định khoảng cách giữa các ô? (A. gap (tên cũ: grid-gap); B. grid-spacing; C. grid-padding; D. grid-margin)
Đáp án: A — gap (trước đây là grid-gap) đặt khoảng cách giữa các hàng và cột; ba tên còn lại không tồn tại.
Câu 4.10. Các thuộc tính nào được dùng trong grid để định nghĩa số cột và hàng? (A. grid-template-columns và grid-template-rows; B. grid-template-rows và grid-template-areas; C. grid-template-columns và grid-template-areas; D. grid-columns và grid-rows)
Đáp án: A — số cột do grid-template-columns quyết định, số hàng (và chiều cao) do grid-template-rows; còn grid-template-areas đặt tên vùng.
Câu 4.11. Các thuộc tính nào được dùng trong grid để xác định vị trí bắt đầu và kết thúc của một phần tử con trên lưới?
Đáp án: A — grid-column-start, grid-column-end, grid-row-start và grid-row-end (có thể viết gọn bằng grid-column, grid-row hoặc grid-area).
Câu 4.12. Có thể sử dụng grid để tạo bố cục đáp ứng không? (A. Có, nhưng chỉ dùng giá trị cố định cho kích thước cột và hàng; B. Không; C. Có, grid hỗ trợ bố cục đáp ứng và các giá trị kích thước động; D. Có, nhưng chỉ dùng grid-template-areas)
Đáp án: C — Grid hỗ trợ kích thước động như fr, minmax(), auto-fit, và có thể kết hợp media query để đổi grid-template-columns hoặc grid-template-areas.
Câu 4.13. Trong CSS Grid, thuộc tính nào được sử dụng để định nghĩa cấu trúc cột của một grid container? (A. grid-template-columns; B. grid-gap; C. grid-template-rows; D. grid-auto-flow)
Đáp án: A — grid-template-columns. grid-gap là khoảng cách, grid-template-rows định nghĩa hàng, grid-auto-flow điều khiển cách xếp tự động.
Sai lầm thường gặp
| Sai lầm | Hậu quả | Cách đúng |
|---|---|---|
Quên display: flex (hoặc grid) trên container mà chỉ đặt justify-content, flex: 1 cho con |
Các thuộc tính không có tác dụng, các con vẫn xếp như khối thường | Đặt display: flex trên phần tử cha; thuộc tính container đặt ở cha, thuộc tính item đặt ở con |
Nhầm hai trục: dùng justify-content để căn dọc trong container row |
Phần tử không căn như mong muốn | justify-content theo main axis, align-items theo cross axis; đổi flex-direction thì hai trục đổi chỗ |
Dùng align-content khi chỉ có một dòng |
Không thấy thay đổi gì | Bật flex-wrap: wrap để có nhiều dòng; một dòng thì dùng align-items |
Muốn các cột bằng nhau nhưng chỉ đặt flex-grow: 1 |
Các cột có nội dung khác nhau ra độ rộng không đều | Đặt flex: 1 (tức 1 1 0%) cho tất cả các cột |
| Để logo/avatar bị bóp méo khi thiếu chỗ | Ảnh biến dạng, chữ dồn | Đặt flex-shrink: 0 (hoặc flex: none) cho phần tử cần giữ kích thước |
Cộng padding vào cột flex: 30% + 70% mà thiếu box-sizing: border-box |
Tổng vượt 100%, cột bị đẩy xuống dòng | Đặt box-sizing: border-box cho các cột (bài 4.2) |
Đếm đường lưới sai: tưởng grid-column: 1 / 3 là hai đường |
Ô chiếm sai số cột | Số sau dấu / là đường kết thúc; 1 / 3 chiếm cột 1 và 2; hoặc dùng 1 / span 2 |
Dùng grid-gap, grid-column-gap trong mã mới |
Tên lỗi thời, có thể bị bỏ trong tương lai | Dùng gap, row-gap, column-gap |
Quên thẻ meta viewport |
Điện thoại hiển thị trang như 980px rồi thu nhỏ; media query không có tác dụng | Thêm <meta name="viewport" content="width=device-width, initial-scale=1.0"> vào head |
Thêm user-scalable=no vào viewport |
Người dùng không phóng to được, vi phạm tiêu chuẩn tiếp cận | Chỉ dùng width=device-width, initial-scale=1.0 |
Dùng device-width trong media query |
Tính năng đã bị loại khỏi tiêu chuẩn | Dùng width, min-width, max-width |
Xếp media query max-width lộn xộn (600px viết trước 800px) |
Quy tắc 800px ghi đè quy tắc 600px nên điện thoại nhận sai bố cục | Giữ thứ tự: với max-width đi từ lớn đến nhỏ; với min-width (mobile first) đi từ nhỏ đến lớn |
Đặt img mặc định ở đầu picture hoặc xếp source nhỏ trước |
Luôn nhận ảnh nhỏ nhất/sai | Trong picture, source điều kiện rộng nhất đặt trước, img đặt cuối |
Tóm tắt
- Flexbox là bố cục một chiều gồm container (
display: flex) và item. Container cóflex-direction,flex-wrap,flex-flow,justify-content(main axis),align-itemsvàalign-content(cross axis); item cóorder,flex-grow,flex-shrink,flex-basis,flex(viết tắt) vàalign-self. - Flexbox chia phần thừa theo
flex-growvà chia phần thiếu theoflex-shrink(tính theo basis);flex: 1cho các cột bằng nhau,flex: 0 0 200pxcho cột cố định. - Grid là bố cục hai chiều (
display: grid) vớigrid-template-columns,grid-template-rows,grid-template(hàng / cột),grid-template-areasvàgap; đơn vịfr, hàmrepeat(),minmax()vàauto-fitgiúp lưới co giãn không cần media query. - Phần tử con của lưới đặt vị trí bằng
grid-column,grid-row(hoặcstart/end/span) vàgrid-area(bốn đường hoặc tên vùng). - Bố cục Website chung gồm header, nav, content (1–3 cột), footer; dựng bằng flex container
flex-wrap: wrapvàflex-basis: 100%cho các phần toàn chiều ngang. - Thiết kế đáp ứng cần
meta viewport(width=device-width, initial-scale=1.0), media queries (@media only screen and (max-width: …)), nguyên tắc Mobile first (viết cho di động trước, mở rộng bằngmin-width), ảnh co giãn (width: 100%,picture/source), menu đổi hàng/cột hoặc Hamburger, ẩn bớt phần tử và đổi cỡ chữ theo màn hình.