Bấm gửi xong mà trang vẫn đứng ở cuối form, ba lỗi nằm đâu đó phía trên, còn những ô vừa nhập lại bị xóa sạch: đây không chỉ là một thông báo chưa đẹp. Người dùng phải tự đoán việc gửi có thành công hay chưa, tìm lỗi bằng cách cuộn ngược và nhập lại dữ liệu. Với form dài, vài giây khó chịu rất nhanh biến thành bỏ cuộc.
Một flow phục hồi tốt cần trả lời ngay bốn câu hỏi: có bao nhiêu lỗi, lỗi ở đâu, sửa thế nào và dữ liệu nào vẫn còn. Error summary, inline error và focus đều phục vụ flow đó, nhưng mỗi phần làm một việc khác nhau. Bài này đối chiếu hướng dẫn W3C với pattern của GOV.UK, sau đó kiểm tra một fixture local ở cả kiểu server-rendered lẫn AJAX.
Mục lục [Hiển thị]
- Người dùng cần biết gì ngay sau submit lỗi?
- Error summary và inline error chia việc ra sao?
- Focus summary hay field lỗi đầu tiên?
- Giữ dữ liệu, nhưng không giữ mọi thứ
- Đừng để screen reader nghe cùng một lỗi hai lần
- Kết quả fixture: điều gì đã được kiểm tra?
- Bảng quyết định để designer và developer bàn giao cùng một flow
Người dùng cần biết gì ngay sau submit lỗi?
WCAG 2.2 Success Criterion 3.3.1 yêu cầu lỗi nhập liệu được nhận diện và mô tả bằng text khi hệ thống tự phát hiện lỗi. Nói gọn: đổi viền sang đỏ là chưa đủ. Người không phân biệt được màu, người đang zoom lớn hoặc người dùng screen reader vẫn cần một câu có nghĩa như “Nhập email có ký tự @, ví dụ an@example.com”. Phần giải thích Error Identification của W3C cũng nhấn mạnh việc mô tả lỗi bằng chữ, không chỉ bằng màu hay một dấu hiệu thị giác.
Thông báo sau submit nên đặt người dùng vào trạng thái có thể hành động tiếp. Một câu “Có lỗi xảy ra” chỉ xác nhận thất bại, nhưng chưa chỉ ra việc cần làm. Nội dung tốt hơn gồm:
- số lỗi hoặc ít nhất một heading rõ rằng form chưa gửi được;
- mỗi lỗi gọi đúng tên field và nói cách sửa;
- đường đi nhanh tới field tương ứng;
- trạng thái dữ liệu đã nhập, nhất là file upload hoặc trường nhạy cảm có thể phải nhập lại.
Đừng bắt người dùng đọc lại toàn bộ form để tìm một chiếc viền đỏ sương sương. Nếu trang reload sau validation phía server, title hoặc heading của trang cũng nên báo trạng thái lỗi. W3C ghi nhận page title hữu ích với screen reader vì nó được thông báo khi trang mới tải lại; đây là một lựa chọn thiết kế, không phải lý do để nhồi toàn bộ danh sách lỗi vào title.
Error summary và inline error chia việc ra sao?
Error summary cho bức tranh toàn cục: form có vấn đề, có mấy điểm cần sửa và thứ tự đi qua chúng. Inline error nằm sát control để giải thích chính xác input nào sai. W3C Forms Tutorial đề xuất danh sách lỗi có heading riêng, mô tả dễ hiểu và link nội trang tới từng control; lỗi cạnh field có thể được liên kết bằng aria-describedby.
Hai lớp này không nên mâu thuẫn. Summary ghi “Email không hợp lệ” nhưng inline lại ghi “Required” thì người dùng phải giải hai câu đố khác nhau. Copy nên thống nhất về nguyên nhân và hướng sửa, còn summary có thể rút gọn vừa đủ để quét nhanh.
<div id="error-summary" tabindex="-1">
<h2>Có 2 lỗi cần sửa</h2>
<ul>
<li><a href="#email">Email cần đúng định dạng</a></li>
</ul>
</div>
<label for="email">Email</label>
<input id="email"
aria-invalid="true"
aria-describedby="email-hint email-error">
<p id="email-error">Nhập email có ký tự @.</p>
tabindex="-1" cho phép script đưa focus tới summary mà không thêm nó vào thứ tự Tab thông thường. Khi người dùng kích hoạt link, field đích cần nhận focus chứ không chỉ cuộn vào viewport. Nếu trang có sticky header, thêm scroll-margin-top cho summary và field để label không bị thanh điều hướng che mất.

Focus summary hay field lỗi đầu tiên?
Không có một đáp án đúng cho mọi form. W3C Forms Tutorial nói việc chuyển focus tới input lỗi đầu tiên là thuận tiện. Trong khi đó, GOV.UK Design System dùng error summary ở đầu trang và JavaScript của component chuyển focus tới summary khi trang chứa lỗi. Chữ “must” trên trang GOV.UK là quy ước của design system đó; đừng biến nó thành một yêu cầu normative duy nhất của WCAG cho mọi sản phẩm.
Với form ngắn chỉ có một lỗi, focus thẳng vào field thường tạo đường đi gọn nhất. Label, hint và inline error phải được đọc theo đúng quan hệ lập trình. Với form dài, nhiều section hoặc nhiều lỗi, summary cho người dùng biết quy mô công việc trước; danh sách link giúp họ chọn lỗi cần sửa và không phải Tab qua hàng chục control hợp lệ.
Điểm cần tránh là tự động nhảy focus khi người dùng còn đang gõ. Validation theo submit hoặc khi rời field dễ dự đoán hơn. Nếu kiểm tra theo từng phím, thông báo live region có thể liên tục cắt ngang tác vụ. Bài về focus indicator WCAG 2.2 giải thích cách giữ focus nhìn thấy rõ; ở flow hiện tại, phần khó hơn là chọn đúng đích focus và thời điểm di chuyển.
Giữ dữ liệu, nhưng không giữ mọi thứ
Validation thất bại không nên biến thành nút reset. Với server-rendered form, response cần render lại giá trị hợp lệ, trạng thái select, checkbox và radio cùng thông báo lỗi. Với AJAX, thường không cần thay toàn bộ form; cập nhật summary và các node lỗi sẽ ít làm mất state hoặc phá focus hơn.
Technique G139 của W3C mô tả một cách triển khai validation phía server: hiển thị lại form cùng dữ liệu đã nhập, mô tả lỗi ở đầu trang và cung cấp link tới field. Trang này cũng nói rất rõ technique là ví dụ để đáp ứng WCAG, không phải cách bắt buộc duy nhất.
Ngoại lệ quan trọng là dữ liệu nhạy cảm. Password, OTP và CVV không nên được điền lại sau lỗi. Trình duyệt cũng hạn chế việc khôi phục file input; nếu upload mất sau reload, giao diện cần nói thẳng “Hãy chọn lại file” thay vì để người dùng tin rằng file vẫn đang chờ gửi. Với checkout hoặc hồ sơ dài, nên bảo toàn dữ liệu không nhạy cảm và chỉ yêu cầu nhập lại phần thực sự cần thiết.
Textarea hiện lỗi có thể làm layout thay đổi khá mạnh. Khi thiết kế field nhiều dòng, có thể tham khảo cách xử lý kích thước ở bài CSS field-sizing cho form tự co giãn, nhưng vẫn cần kiểm tra lại ở trạng thái có hint và error text. Một field đẹp khi chưa có lỗi chưa chắc còn ổn khi thêm hai dòng giải thích.
Đừng để screen reader nghe cùng một lỗi hai lần
AJAX cần cơ chế báo rằng DOM vừa thay đổi. W3C đưa ra ví dụ container danh sách lỗi dùng role="alert"; tutorial cũng minh họa aria-live="polite" cho phản hồi khi gõ. Vấn đề xuất hiện khi giao diện vừa focus summary, vừa chèn cùng nội dung vào một live region assertive. Tùy tổ hợp browser và screen reader, thông báo có thể bị đọc lặp hoặc cắt ngang.
Chọn cơ chế theo flow thay vì cộng tất cả thuộc tính ARIA cho chắc. Trang reload phía server có thể dựa vào title, heading và focus có chủ đích. AJAX có thể dùng live region hoặc role="alert" để thông báo thay đổi, nhưng cần test cùng focus movement. ARIA không thay thế semantic HTML; link vẫn phải là link, label vẫn phải trỏ đúng control.
Bài này chưa chạy screen reader nên không khẳng định câu nào được đọc, thứ tự ra sao hay có lặp trên NVDA/JAWS/VoiceOver. Đó là hạng mục cần test thủ công trên tổ hợp sản phẩm thực tế trước khi ship. Kết quả browser automation chỉ xác nhận DOM, focus và state, không thể đại diện cho output âm thanh.
Kết quả fixture: điều gì đã được kiểm tra?
Fixture local dùng dữ liệu giả, bốn field và ba mode: server-rendered, AJAX và negative control. Chromium chạy headless trên Windows. Form dài chuyển focus tới summary; link “Email cần đúng định dạng” chuyển focus tiếp tới input email. Form ngắn chuyển thẳng tới field lỗi đầu tiên. Cả hai mode giữ họ tên và ghi chú, đồng thời xóa password.
Negative control chỉ đổi viền đỏ, không có error text, không có summary, không đổi focus và xóa dữ liệu. Bộ test phát hiện đủ các lỗi đó. AJAX tạo đúng một summary sau submit và không xóa dữ liệu thường. Ở viewport 390 px và kiểm tra reflow 320 px, fixture không có horizontal overflow. 320 px ở đây là phép kiểm reflow gần với không gian CSS khi phóng lớn, không thay cho việc test zoom và assistive technology đầy đủ trên trình duyệt thật.

Firefox không chạy vì môi trường chưa có executable tương ứng, nên không có kết quả Firefox để báo. Việc ghi rõ thiếu coverage quan trọng hơn một câu “đã test đa trình duyệt” nghe rất ổn nhưng không có bằng chứng. Trên sản phẩm thật, test matrix tối thiểu nên có Chrome/Edge, Firefox, Safari nếu hỗ trợ, keyboard-only, zoom 200%/400% và ít nhất một screen reader đang nằm trong support matrix.
Bảng quyết định để designer và developer bàn giao cùng một flow
| Tình huống | Focus sau submit | Thông báo nên có | Dữ liệu |
|---|---|---|---|
| Form ngắn, một lỗi | Field lỗi đầu tiên | Inline error rõ cách sửa | Giữ field không nhạy cảm |
| Form dài, nhiều lỗi | Error summary | Summary có link + inline error | Giữ state theo từng control |
| AJAX update | Summary hoặc field theo cấu trúc form | Live announcement có chủ đích | Không thay cả form nếu không cần |
| Password, OTP, CVV | Theo flow chung | Nói rõ cần nhập lại | Không tự điền lại |
| File upload mất sau reload | Summary hoặc upload field | Báo phải chọn lại file | Không giả rằng file còn giữ |
Trong handoff, designer nên ghi copy cho summary và inline error, target focus, hành vi ở sticky header, state nào được giữ và ngoại lệ dữ liệu nhạy cảm. Developer bổ sung quan hệ label/error, logic focus, cách announce DOM update và test keyboard. Màu đỏ vẫn phải đủ tương phản, nhưng màu không thay cho text; bài CSS contrast-color() cũng đi đến cùng một nguyên tắc: tự chọn màu chữ không xóa trách nhiệm kiểm tra tương phản và ngữ nghĩa.
Chốt lại, error summary không phải miếng vá đặt lên đầu form. Nó là điểm bắt đầu của một đường phục hồi: báo rõ trạng thái, đưa người dùng tới đúng field, giữ phần dữ liệu có thể giữ và tránh đọc lặp. Form ngắn có thể focus lỗi đầu tiên; form dài thường đáng dùng summary. Quan trọng nhất là test cả flow từ submit tới lần gửi lại, vì đời không như mơ ở đúng đoạn giữa hai lần bấm nút đó.