Mở đầu — vì sao bài này quan trọng
Hãy thử một thí nghiệm nhỏ ngay bây giờ. Mở trang web bạn vừa thiết kế gần nhất, đặt tay rời khỏi chuột, và chỉ dùng phím Tab để di chuyển qua từng phần tử. Bạn có biết mình đang đứng ở đâu không? Cái nút "Đặt hàng" có sáng lên một viền nào đó để bạn biết nó đang được chọn không? Bạn có nhấn Enter mở được menu, dùng mũi tên đi xuống được trong dropdown không?
Nếu câu trả lời là "không rõ" hoặc "viền nó nhảy lung tung", thì bạn vừa chạm vào một trong những lỗ hổng phổ biến nhất — và bị bỏ qua nhiều nhất — của thiết kế web Việt Nam: keyboard navigation (điều hướng bằng bàn phím) và focus states (trạng thái khi một phần tử đang được chọn).
Đây không phải chuyện "làm cho có" để tích vào ô accessibility. Có ba nhóm người phụ thuộc trực tiếp vào nó:
- Power user — lập trình viên, kế toán, nhân viên nhập liệu. Họ gõ nhanh hơn rê chuột. Một bạn kế toán nhập 400 dòng hóa đơn mỗi ngày sẽ không bao giờ chạm vào chuột nếu form của bạn cho phép Tab mượt mà.
- Người khuyết tật vận động — người không cầm được chuột, dùng bàn phím hoặc thiết bị switch (công tắc) làm phương tiện duy nhất để dùng máy tính.
- Người dùng screen reader (trình đọc màn hình) — người khiếm thị "nghe" trang web, và họ điều hướng hoàn toàn bằng bàn phím.
Khái niệm cốt lõi
Focus là gì?
Trong trình duyệt, tại bất kỳ thời điểm nào cũng chỉ có đúng một phần tử đang nhận "focus" — tức là nơi mà bàn phím đang trỏ tới. Khi bạn gõ, ký tự đi vào phần tử đó. Khi bạn nhấn Enter, hành động xảy ra trên phần tử đó. Focus giống như con trỏ chuột nhưng dành cho bàn phím — và khác con trỏ chuột ở chỗ: nó vô hình nếu bạn không chủ động làm cho nó hiện ra.
Tab order — thứ tự duyệt
Khi người dùng nhấn Tab, focus di chuyển tới phần tử "có thể focus" tiếp theo. Shift + Tab đi lùi lại. Mặc định, thứ tự này đi theo trình tự phần tử trong mã HTML (DOM order), từ trên xuống dưới, trái sang phải.
Đây là điểm mấu chốt mà nhiều designer không biết: thứ tự Tab = thứ tự HTML, KHÔNG phải thứ tự bạn nhìn thấy trên màn hình. Nếu bạn dùng CSS (ví dụ order trong Flexbox hay đặt vị trí bằng Grid) để xếp một nút lên trên cùng về mặt thị giác, nhưng trong HTML nó nằm cuối, thì người dùng bàn phím vẫn phải Tab qua tất cả mọi thứ mới tới được nó. Mắt nhìn thấy một đằng, bàn phím đi một nẻo.
Những phần tử nào focus được?
Theo mặc định, chỉ các phần tử tương tác mới nhận được focus qua phím Tab:
<a href="...">— link có thuộc tính href<button><input>,<textarea>,<select>- Các phần tử có
tabindex
<div> thường không focus được. Đây là lý do cực kỳ quan trọng để bạn dùng đúng thẻ HTML semantic: một cái nút làm bằng <div onclick="..."> trông giống nút thật, bấm chuột vào vẫn chạy, nhưng người dùng bàn phím không bao giờ Tab tới được nó. Nó vô hình với họ.Focus indicator — viền focus
Khi một phần tử nhận focus, trình duyệt vẽ một viền quanh nó — thường là một đường outline xanh hoặc chấm. Đây gọi là focus indicator hoặc focus ring. Nó là cách duy nhất để người dùng bàn phím biết "tôi đang đứng ở đây".
Và đây là tội lỗi kinh điển nhất trong thiết kế web. Vì designer thấy viền outline mặc định của trình duyệt "xấu", nên dòng CSS đầu tiên rất nhiều người viết là:
*:focus { outline: none; }
Một dòng này vừa giết chết toàn bộ khả năng dùng bàn phím của cả website. Người dùng vẫn Tab được, nhưng không nhìn thấy mình ở đâu — như đi trong bóng tối. Đây là vi phạm trực tiếp tiêu chí WCAG 2.4.7 Focus Visible.
:focus vs :focus-visible
Đây là kiến thức "nâng cấp" rất đáng giá. Vấn đề khiến nhiều designer ghét viền focus là: nó cũng hiện ra khi bạn bấm chuột vào nút, trông thừa thãi. Trình duyệt hiện đại giải quyết bằng pseudo-class :focus-visible:
:focus— kích hoạt mỗi khi phần tử có focus, bất kể nguồn (chuột hay bàn phím).:focus-visible— trình duyệt chỉ kích hoạt khi nó đoán rằng người dùng cần thấy viền, tức là khi điều hướng bằng bàn phím, chứ không hiện khi bấm chuột.
Focus trap và focus management
Với các thành phần như modal, dropdown, menu, có hai khái niệm quan trọng:
- Focus trap (bẫy focus): khi một modal mở ra, focus phải bị "giữ" bên trong nó. Tab tới phần tử cuối của modal rồi nhấn Tab nữa phải quay lại phần tử đầu, chứ không được "thoát" ra trang phía sau.
- Focus return (trả focus): khi đóng modal, focus phải quay về đúng cái nút đã mở nó, để người dùng không bị "lạc" về đầu trang.
Tình huống thực tế
Ví dụ 1 — Tiki và bạn kế toán nhập đơn hàng
Hãy hình dung một sàn thương mại như Tiki, trong trang quản lý đơn hàng dành cho nhà bán (seller dashboard). Chị Lan, kế toán của một shop mỹ phẩm, mỗi sáng nhập tay khoảng 150 đơn từ Zalo vào hệ thống. Mỗi đơn có 6 ô: tên khách, số điện thoại, địa chỉ, sản phẩm, số lượng, ghi chú.
Phiên bản đầu, các ô input được sắp xếp đẹp mắt bằng CSS Grid hai cột, nhưng trong HTML thứ tự lại là: tên → địa chỉ → SĐT → ghi chú → sản phẩm → số lượng. Khi chị Lan gõ xong tên rồi nhấn Tab, con trỏ nhảy xuống ô "địa chỉ" ở góc dưới bên phải — không phải ô "số điện thoại" ngay bên cạnh như mắt chị thấy. Chị buộc phải bỏ tay khỏi bàn phím, rê chuột tìm lại. Mỗi đơn mất thêm 8 giây. 150 đơn = 20 phút lãng phí mỗi ngày, cộng với mệt mỏi.
Khi đội kỹ thuật sửa lại thứ tự HTML khớp với thứ tự thị giác và đảm bảo nút "Lưu đơn" focus được bằng phím Enter, chị Lan nhập trọn một đơn mà không chạm chuột lần nào. Năng suất tăng rõ rệt.
Bài học: Tab order là một tính năng năng suất, không chỉ là accessibility. Power user Việt Nam — kế toán, telesales, nhập liệu — là nhóm khổng lồ và họ cảm nhận lỗi này ngay lập tức.
Ví dụ 2 — Anh Tuấn khiếm thị mua vé tàu trực tuyến
Anh Tuấn ở Đà Nẵng bị khiếm thị, dùng screen reader NVDA để lướt web. Anh muốn đặt vé tàu trên một website bán vé. Trang có một nút "Chọn ghế" trông rất đẹp, nhưng nó được code bằng <div class="btn" onclick="openSeatMap()">.
Vì là <div>, screen reader không đọc nó là "nút", và phím Tab không bao giờ dừng lại ở đó. Anh Tuấn Tab qua cả trang mà không hề biết có chức năng chọn ghế tồn tại. Với anh, website này coi như không bán vé được. Anh đành gọi điện tổng đài — tốn thời gian cho cả hai bên.
Đội thiết kế đổi <div> thành <button> thật, thêm nhãn rõ ràng. Ngay lập tức screen reader đọc "Chọn ghế, nút", Tab dừng đúng chỗ, Enter mở được sơ đồ ghế. Một thay đổi nhỏ về thẻ HTML, nhưng mở ra cả một nhóm khách hàng.
Bài học: Dùng <button> và <a> đúng nghĩa. Đừng giả nút bằng <div>. Semantic HTML chính là nền tảng của keyboard accessibility.
Ví dụ 3 — Một startup fintech và cái modal "không thoát ra được"
Một startup ví điện tử ở TP.HCM ra mắt tính năng xác nhận giao dịch bằng một modal popup. Nhưng họ quên xử lý focus. Khi modal mở, focus vẫn nằm ở trang phía sau. Người dùng bàn phím nhấn Tab thì con trỏ chui ra sau lớp mờ, "lạc" vào các link ở trang nền — vốn đang bị che. Tệ hơn, phím Esc không đóng được modal.
Trong một buổi user testing với một bạn dùng bàn phím, bạn ấy mở modal rồi mắc kẹt hoàn toàn: không tới được nút "Xác nhận", không đóng được, phải tải lại cả trang. Giao dịch thất bại.
Sau đó đội bổ sung ba thứ: (1) khi mở modal, focus tự nhảy vào nút đầu tiên bên trong; (2) Tab bị giữ trong modal (focus trap); (3) Esc đóng modal và trả focus về nút "Thanh toán" đã mở nó. Trải nghiệm trở nên mượt và an toàn.
Bài học: Bất kỳ lớp giao diện nổi nào (modal, drawer, dropdown) đều cần quản lý focus chủ động. Mở thì đưa focus vào, đóng thì trả focus về, và luôn cho phép Esc.
Hướng dẫn từng bước
Đây là quy trình kiểm tra và sửa keyboard navigation cho một trang bất kỳ:
Bước 1 — Test bằng tay. Bỏ chuột ra. Dùng Tab đi từ đầu đến cuối trang. Ghi lại: focus có nhìn thấy ở mọi bước không? Thứ tự có hợp lý không? Có "cái nút" nào Tab không tới được không?
Bước 2 — Dùng semantic HTML. Mọi thứ bấm được phải là <button> hoặc <a>. Tránh <div onclick>. Nếu bắt buộc phải dùng phần tử không tương tác, thêm tabindex="0" để nó focus được và xử lý sự kiện phím Enter/Space — nhưng đây là phương án cuối.
Bước 3 — Khớp thứ tự HTML với thứ tự thị giác. Đừng dựa vào tabindex dương (như tabindex="3") để sắp lại thứ tự — nó tạo ra mớ hỗn độn khó bảo trì. Thay vào đó, sắp lại chính cấu trúc HTML cho đúng, rồi dùng CSS để trình bày.
Bước 4 — Cho focus indicator thật rõ. Thay vì xóa outline, hãy thiết kế lại nó cho đẹp và rõ:
:focus-visible {
outline: 3px solid #0066ff;
outline-offset: 2px;
border-radius: 4px;
}
outline-offset đẩy viền ra xa một chút cho thoáng. Dùng :focus-visible để chỉ hiện khi cần.
Bước 5 — Đảm bảo độ tương phản của viền focus. Viền focus cũng phải nhìn rõ trên nền của nó. WCAG yêu cầu tối thiểu tỉ lệ tương phản 3:1 giữa viền focus và nền xung quanh (tiêu chí 1.4.11). Đừng dùng viền xám nhạt trên nền trắng.
Bước 6 — Thêm "Skip to content" link. Ở đầu trang, đặt một link ẩn "Bỏ qua tới nội dung chính", chỉ hiện khi được focus. Nó giúp người dùng bàn phím khỏi phải Tab qua cả menu dài mỗi lần đổi trang:
<a href="#main" class="skip-link">Bỏ qua tới nội dung chính</a>
.skip-link {
position: absolute;
left: -9999px;
}
.skip-link:focus {
left: 1rem;
top: 1rem;
}
Bước 7 — Xử lý các phím chuẩn cho component tương tác. Người dùng kỳ vọng: Enter/Space để kích hoạt nút; mũi tên để đi trong menu, tab list, radio group; Esc để đóng modal/dropdown. Tuân theo các quy ước này (WAI-ARIA Authoring Practices) để không làm người dùng bối rối.
Lỗi thường gặp & mẹo
Lỗi 1 — outline: none không kèm thay thế. Đây là lỗi số một. Nếu bạn xóa outline mặc định, bạn bắt buộc phải cung cấp một focus indicator khác (đổi nền, thêm box-shadow, viền dày hơn). Đừng bao giờ để focus vô hình.
Lỗi 2 — Giả nút bằng <div> / <span>. Trông giống, bấm chuột chạy, nhưng vô hình với bàn phím và screen reader. Luôn dùng <button>.
Lỗi 3 — Lạm dụng tabindex dương. tabindex="1", tabindex="2"... ép thứ tự Tab và phá vỡ luồng tự nhiên, cực kỳ khó bảo trì. Chỉ dùng tabindex="0" (đưa vào luồng tự nhiên) và tabindex="-1" (focus được bằng code nhưng không bằng Tab).
Lỗi 4 — Bẫy focus ngoài ý muốn. Đôi khi một widget (như trình soạn thảo nhúng) "nuốt" phím Tab khiến người dùng không thoát ra được. Khác với focus trap có chủ đích trong modal, đây là lỗi. Hãy đảm bảo người dùng luôn Tab ra được khỏi mọi component không phải modal.
Lỗi 5 — Focus chạy ra phần tử bị ẩn. Khi bạn ẩn một menu bằng opacity: 0 hoặc visibility mà quên xử lý, các link bên trong vẫn focus được — Tab "biến mất" vào một vùng vô hình. Dùng display: none cho phần ẩn, hoặc gỡ chúng khỏi tab order.
Mẹo — Test focus order bằng DevTools. Tab order khó hình dung khi nhìn code. Hãy cứ test thủ công bằng phím Tab — đó là cách nhanh và đáng tin nhất. Một số extension như "Accessibility Insights" còn vẽ đường đi của tab order lên màn hình.
Mẹo — Đừng làm viền focus quá mảnh. Một outline 1px màu nhạt gần như vô dụng với người thị lực kém. Tối thiểu 2-3px và màu tương phản tốt.
Bài tập thực hành
- Audit bằng bàn phím: Chọn một trang web Việt Nam bạn hay dùng (Tiki, Shopee, VNExpress, hay trang của bạn). Cất chuột đi, chỉ dùng Tab + Shift+Tab + Enter + Esc để duyệt toàn trang. Ghi lại 3 vấn đề: chỗ nào focus biến mất, chỗ nào Tab tới mà không bấm được, chỗ nào thứ tự sai.
- Thiết kế focus ring riêng: Tạo một trang đơn giản với 3 nút và 2 ô input. Viết CSS dùng
:focus-visibleđể tạo một focus indicator thật đẹp, rõ, đạt tương phản ≥ 3:1. Kiểm tra: viền có hiện khi Tab, và không hiện khi bấm chuột.
- Sửa một
<div>giả nút: Tìm (hoặc tự tạo) một<div onclick>đóng vai nút. Đổi thành<button>thật và xác nhận: Tab tới được, Enter và Space đều kích hoạt, screen reader (bật VoiceOver trên Mac hoặc NVDA trên Windows) đọc đúng là "nút".
- Thêm skip link: Thêm một "Bỏ qua tới nội dung chính" vào đầu trang bài tập, ẩn cho đến khi được focus. Test bằng cách nhấn Tab lần đầu khi vừa tải trang.
Tóm tắt
- Focus là vị trí bàn phím đang trỏ tới — chỉ một phần tử tại một thời điểm, và nó vô hình nếu bạn không làm cho nó hiện ra.
- Tab order đi theo thứ tự HTML, không theo bố cục thị giác. Hãy khớp hai thứ này với nhau bằng cách sắp đúng HTML, đừng dùng
tabindexdương. - Mọi thứ bấm được phải là
<button>hoặc<a>semantic, không giả nút bằng<div>. - Đừng bao giờ
outline: nonemà không thay thế. Dùng:focus-visibleđể tạo focus indicator đẹp, rõ, tương phản ≥ 3:1, chỉ hiện khi điều hướng bằng bàn phím. - Các lớp nổi (modal, dropdown) cần quản lý focus chủ động: đưa focus vào khi mở, giữ focus bên trong, trả focus về và cho Esc đóng khi đóng.
- Thêm skip link, tuân theo quy ước phím chuẩn (Enter/Space/mũi tên/Esc), và luôn test bằng cách bỏ chuột ra.