CSS tốt không chỉ làm giao diện đẹp; nó giữ cho nội dung còn đọc được khi browser đổi mode, viewport co lại và người dùng phóng to chữ.
Bài này cô đọng cách tôi dùng CSS thuần trên site Astro static: token cho browser mode, breakpoint theo chỗ layout chật, và Markdown có thể reflow trên mobile.
🧭 Mô hình tổng thể
Browser cung cấp preference; token giữ ngôn ngữ giao diện; layout và content cùng bảo toàn đường đọc.
Browser preference
|
v
CSS tokens -> layout rules -> readable content
viewport đổi, ý và chức năng còn nguyên
Ở site này, src/styles/global.css chứa token, layout chung và breakpoint. :root giữ mặc định; prefers-color-scheme: dark chỉ override token. Component chỉ dùng var(--paper), var(--ink), var(--muted) và var(--line).
Responsive không phải làm mọi thứ nhỏ đi mà là đổi cách trình bày khi không gian đổi, trong khi nội dung và chức năng còn nguyên. Đây là tinh thần của WCAG 2.2, Success Criterion 1.4.10 Reflow; bảng, code và canvas có thể là ngoại lệ có lý do (W3C: Understanding Reflow).
🌗 Quản lý CSS theo browser mode
1. Chọn một nguồn sự thật cho màu
Không rải mã màu vào từng selector; đặt chúng thành semantic token:
:root {
color-scheme: light dark;
--paper: #e9e6de;
--ink: #141517;
--muted: #55585b;
--line: #aaa9a3;
--panel: rgba(255, 255, 255, 0.3);
--focus: #135fcc;
}
@media (prefers-color-scheme: dark) {
:root {
--paper: #17191d;
--ink: #e8e5dd;
--muted: #b1afa9;
--line: #45484d;
--panel: rgba(255, 255, 255, 0.035);
--focus: #8bb8ff;
}
}
--paper, --ink và --muted mô tả vai trò, không mô tả màu. Đổi mode chỉ cần đổi token; card dùng var(--panel) sẽ tự theo mode.
prefers-color-scheme đọc preference của hệ điều hành hoặc browser (MDN: prefers-color-scheme). Tôi coi đây là default của môi trường, không phải state để application tự đoán.
2. Đừng dùng màu làm tín hiệu duy nhất
Dark mode không chỉ là đảo trắng thành đen. Tôi kiểm tra thêm:
- Chữ phụ, link, focus, hover và trạng thái chọn phải còn nhận ra được.
- Focus cần outline riêng bằng
:focus-visible; hình, code, bảng và quote cần surface semantic riêng. - Kiểm tra cả trạng thái bình thường, focus, disabled, menu mở và link đã truy cập.
Tôi thường mở DevTools, chuyển prefers-color-scheme, tab qua các control rồi kiểm tra từng trạng thái. Nếu chỉ nhìn screenshot ở trạng thái bình thường, tôi dễ bỏ sót focus, disabled, menu mở hoặc link đã visited.
3. Giữ mode logic ở một chỗ
Một số rule là hợp đồng toàn site:
* {
box-sizing: border-box;
}
body {
min-width: 320px;
background: var(--paper);
color: var(--ink);
}
:focus-visible {
outline: 3px solid var(--focus);
outline-offset: 4px;
}
box-sizing tránh box phình do padding; min-width: 320px nêu ngưỡng kiểm tra; focus global giữ keyboard focus luôn thấy được.
Không tạo hai cây CSS riêng cho light và dark vì dễ lệch spacing. Chỉ dùng selector riêng khi asset thực sự khác theo mode.
📐 Responsive: quản lý theo layout, không theo tên máy
1. Bắt đầu từ layout hỏng ở đâu
Breakpoint không phải danh sách iPhone, tablet, desktop; đó là nơi layout bắt đầu mất nghĩa hoặc khó đọc. Ở site này: >=1120px giữ navigation và reading layout rộng; dưới 880px sidebar/grid xếp dọc; dưới 608px card/list về một cột và menu gọn.
Mỗi mốc phải giải quyết một thay đổi thật như nav ẩn/hiện hoặc grid chuyển cột; không thêm breakpoint chỉ vì con số phổ biến.
2. Ưu tiên flow tự co trước khi thêm media query
Tôi dùng các primitive cho phép layout tự thích nghi:
.page-shell {
width: min(100% - 2rem, 76rem);
min-width: 0;
margin-inline: auto;
}
.prose {
min-width: 0;
font-size: clamp(1.15rem, 1.8vw, 1.28rem);
}
.article-list {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
Ba rule chính:
minmax(0, 1fr)cho phép grid item co thật sự.min-width: 0ngăn text, link dài hoặc child grid đẩy layout tràn.clamp()tạo chuyển tiếp mượt cho cỡ chữ và spacing.
Media query chỉ xử lý quyết định cấu trúc:
@media (max-width: 608px) {
.article-list {
grid-template-columns: 1fr;
}
.article-card {
padding-inline: 0;
border-right: 0;
}
}
Không dùng overflow-x: hidden để giấu lỗi vì có thể cắt mất nội dung hoặc control. Bảng/code được cuộn trong chính vùng của nó; phần còn lại vẫn đọc dọc.
3. Xác định rõ ngoại lệ cần cuộn ngang
Bảng và code là ngoại lệ có chủ đích; ép vừa mobile thường làm mất nghĩa:
.prose pre,
.prose table {
max-width: 100%;
overflow-x: auto;
}
Mỗi ngoại lệ cần xác định rõ vùng cuộn, phần tử cha có còn trong viewport không, và liệu bảng có thể rút gọn hoặc giải thích bằng prose hay không.
4. Test theo trạng thái, không chỉ theo chiều rộng
Checklist tôi chạy khi sửa CSS:
- 320–375 CSS px: không có page-level horizontal scroll; title, link dài và button không bị cắt.
- 608 px: danh sách/card đã đổi đúng cấu trúc, không còn border desktop thừa.
- 880 px: sidebar, TOC và content không chen nhau.
- 1120 px: desktop navigation xuất hiện nhưng không đẩy logo hoặc search tràn.
- Browser zoom 200–400%: nội dung không biến mất; có thể đọc bằng scroll dọc, phù hợp mục tiêu Reflow của WCAG.
- Light và dark mode: contrast, border, code, quote, table, hover, focus đều còn nghĩa.
-
prefers-reduced-motion: reduce: animation/transition được giảm; content không phụ thuộc vào animation để xuất hiện.
✍️ Rule viết content để thân thiện với mobile
CSS chỉ cứu layout; content vẫn phải có cấu trúc tốt.
Rule 1: Một heading phải mở ra một ý có thể đọc độc lập
Giữ heading ngắn, nói rõ khái niệm hoặc hành động; chi tiết để ở paragraph đầu. Dùng đúng thứ bậc H1 → H2 → H3.
Rule 2: Một paragraph nên có một nhiệm vụ
Tách paragraph khi đổi giữa fact, giải thích, ví dụ hoặc khuyến nghị. Mỗi block nên giữ một ý khi zoom lớn hoặc đọc trên màn hình hẹp.
Rule 3: Viết câu để không phụ thuộc vào chiều ngang
- Tránh chuỗi danh từ, dấu
/và inline code quá dài. - URL nên có nhãn mô tả; không dùng khoảng trắng để căn cột.
- Chỉ dùng list cho các item cùng cấp; dùng bảng hoặc code fence đúng vai trò.
Rule 4: Bảng là ngoại lệ, không phải mặc định
Chỉ dùng bảng khi cần so sánh cùng một tập thuộc tính; nếu không, dùng list hoặc heading ngắn:
- Header phải tự mô tả; cell không chứa cả một bài văn.
- Giữ bảng trong vùng
overflow-x: autovà diễn giải ý chính ngoài vùng cuộn.
Rule 5: Code phải có đường đọc trên mobile
Code block được phép cuộn ngang khi indentation hoặc cú pháp là một phần ý nghĩa. Prose trước code phải nêu mục tiêu; dòng quá dài nên tách biến hoặc bỏ phần không liên quan.
Rule 6: Không truyền đạt trạng thái chỉ bằng màu hoặc vị trí
“Màu đỏ là lỗi” là chưa đủ: thêm nhãn Lỗi, icon có text hoặc câu giải thích. Gọi đúng tên section thay vì “cột bên phải”.
Rule 7: Link và media phải có ngữ cảnh
Link cần nhãn mô tả như “đọc hướng dẫn Reflow của W3C”. Ảnh cần alt theo vai trò; video, sơ đồ và code cần prose nói rõ điều cần rút ra.
✅ Quality gate tối thiểu
Không có một checklist CSS duy nhất cho mọi site. Với website content, tôi dùng baseline sau, lấy Reflow và keyboard/focus của WCAG làm mốc:
| Điểm chính | Hành động | Khi nào đạt |
|---|---|---|
| Reflow | Test 320–375 px và zoom lớn | Không mất nội dung/chức năng; trang đọc theo chiều dọc |
| Container | Dùng width: min(...), min-width: 0, max-width |
Không có page-level horizontal scroll |
| Typography/grid | Dùng rem, clamp() và đổi cột theo không gian |
Text không buộc zoom liên tục; card không tràn |
| Table/code | Cuộn ngang ở vùng ngoại lệ | Không kéo ngang cả trang để đọc paragraph |
| Browser mode | Kiểm tra light/dark và token semantic | Text, border, focus, quote, code còn tương phản |
| Keyboard/motion | Tab qua control; test reduced motion | Focus thấy rõ; không mất nội dung/trạng thái |
| Content | Quét heading, paragraph, link và alt | Không phụ thuộc màu hoặc vị trí |
Đây là definition of ready: chưa cần pixel-perfect, nhưng không được mất đường đọc cơ bản.
🧪 Quy trình áp dụng ngay cho một thay đổi CSS
Lỗi đo được (vd. 360 px bị tràn)
-> kiểm tra token + container trước
-> chỉ thêm media query khi đổi cấu trúc
-> test mode, keyboard, motion, content thật
-> cuộn ngang riêng; ghi viewport
-> format/validate
Selector ngoại lệ nhiều? Sửa model trước.
🧾 Kết luận
Browser mode -> token semantic
Responsive -> layout đổi khi chật
Content -> heading, prose, bảng/code
|
v
Không mất nội dung, chức năng, đường hiểu
Quản lý CSS bắt đầu từ token semantic; responsive bắt đầu từ lúc layout chật; content thân thiện mobile bắt đầu từ heading, paragraph, bảng/code và trạng thái không phụ thuộc màu.
Nguyên tắc chung là: không gian có thể đổi, nhưng người đọc không được mất nội dung, chức năng và đường hiểu chính.
🧳 Tóm tắt bỏ túi (Take away)
| Điểm chính | Hành động |
|---|---|
| Browser mode | Đặt màu vào semantic tokens; override token trong prefers-color-scheme |
| Responsive | Để layout tự co trước; chỉ thêm breakpoint khi cấu trúc thật sự cần đổi |
| Mobile content | Heading ngắn, paragraph một ý, link có nhãn, không căn bằng khoảng trắng |
| Bảng và code | Giữ trong scroll container riêng; không làm tràn cả trang |
| Quality gate | Test 320–375 px, zoom, keyboard, light/dark và reduced motion trước khi chốt |