Thông báo & live region
Chọn đúng kênh — validation tại chỗ, MessageBar, Toast hay Dialog — và bảo đảm trình đọc màn hình nghe được thông báo đúng lúc, đúng mức độ.
Chọn kênh thông báo
Mức gián đoạn phải tương xứng với mức quan trọng. Bắt đầu từ kênh ít gián đoạn nhất còn đủ để người dùng không bỏ lỡ.
| Kênh | Khi nào | Gián đoạn | Trình đọc màn hình |
|---|---|---|---|
| Validation tại chỗ (Field) | Lỗi hoặc gợi ý gắn với một ô nhập. | Thấp | Đọc khi focus vào ô (aria-describedby). |
| MessageBar | Trạng thái của trang / vùng: cảnh báo hạn mức, chế độ chỉ đọc, lỗi tải dữ liệu. Ở lại tới khi xử lý xong. | Trung bình | role=alert (error, warning) hoặc role=status (info, success). |
| Toast | Phản hồi ngắn cho hành động vừa làm: đã lưu, đã gửi, đã sao chép. Không chặn thao tác. | Thấp | Vùng thông báo của Radix; F8 để tới. |
| Dialog modalType="alert" | Cần quyết định ngay, thường là phá huỷ hoặc không hoàn tác được. | Cao (chặn) | role=alertdialog, focus vào bề mặt. |
| Live region (AriaLiveAnnouncer) | Thay đổi không có thông báo nhìn thấy: lọc xong, tải thêm, đếm kết quả. | Không | Chỉ đọc, không hiển thị. |
{/* Lỗi gắn với một ô */}
<Field label="Mã giảm giá" validationState="error" validationMessage="Mã đã hết hạn ngày 30/9.">
<Input defaultValue="SAKURA30" />
</Field>
{/* Trạng thái của cả trang / vùng, tồn tại tới khi được xử lý */}
<MessageBar
intent="warning"
title="Bộ nhớ sắp đầy."
actions={<Button size="small">Nâng cấp</Button>}
>
Bạn đã dùng 92% dung lượng.
</MessageBar>
{/* Phản hồi ngắn cho hành động vừa làm */}
toast({ intent: "success", title: "Đã lưu thay đổi" });
{/* Cần quyết định ngay, chặn mọi thao tác khác */}
<DialogSurface modalType="alert">
<DialogTitle action={null}>Xoá dự án “Sakura”?</DialogTitle>
…
</DialogSurface>polite & assertive
Live region quyết định trình đọc màn hình đọc thay đổi lúc nào: polite chờ người dùng rảnh, assertive cắt ngang ngay.
| Mức | Thuộc tính | Component Yugen | Dùng cho |
|---|---|---|---|
| Polite | role="status" · aria-live="polite" | MessageBar info / success; Toast info / success; announce() mặc định | Xác nhận, kết quả, tiến độ. |
| Assertive | role="alert" · aria-live="assertive" | MessageBar error / warning; Toast error / warning; announce(…, { politeness: "assertive" }) | Lỗi chặn công việc, mất dữ liệu, hết phiên. |
{/* Polite: đọc khi trình đọc màn hình rảnh — kết quả, trạng thái, xác nhận */}
<div role="status">Đã lưu bản nháp lúc 14:05</div>
{/* Assertive: ngắt lời ngay — chỉ cho lỗi và cảnh báo cần xử lý */}
<div role="alert">Mất kết nối. Thay đổi chưa được lưu.</div>
{/* Vùng live phải có sẵn trong DOM TRƯỚC khi đổi nội dung */}
<p role="status" className="sr-only">{message}</p>Một số tổ hợp trình đọc màn hình bỏ qua vùng live được chèn vào DOM cùng lúc với nội dung của nó. Với MessageBar hiện ra sau một hành động, giữ một khung chứa luôn có mặt và chỉ thay phần bên trong — hoặc gọi thêm announce() — để chắc chắn được đọc.
Đừng tự đóng điều quan trọng
Toast mặc định tự đóng sau 3 giây. Người đọc chậm, người phóng to màn hình, người dùng trình đọc màn hình đều có thể bỏ lỡ (WCAG 2.2.1).
import { toast } from "@/components/ui";
// Thành công, không có hành động: tự đóng sau 3 giây là hợp lý.
toast({ intent: "success", title: "Đã sao chép liên kết" });
// Lỗi hoặc có hành động: giữ lại tới khi người dùng đóng.
toast({
intent: "error",
title: "Không thể đồng bộ",
body: "Kiểm tra kết nối mạng rồi thử lại.",
timeout: -1,
action: {
label: "Thử lại",
// Người dùng trình đọc màn hình có thể không kịp tới toast — chỉ cách làm khác.
altText: "Thử đồng bộ lại bằng nút Đồng bộ trên thanh công cụ",
onClick: retrySync,
},
});- Rê chuột hoặc focus vào toast sẽ tạm dừng đồng hồ; F8 đưa focus tới vùng thông báo.
- Hành động trong toast phải có cách khác để làm (nút trên trang, menu);
altTextnói cho người dùng trình đọc màn hình biết cách đó. - Lỗi chặn công việc thuộc về MessageBar hoặc validation tại chỗ, không phải toast.
Kết quả bất đồng bộ
Khi nội dung thay đổi mà không có thông báo nhìn thấy — lọc danh sách, tải thêm, lưu tự động — hãy đọc kết quả qua live region.
10 kết quả
- Hà Nội
- Hải Phòng
- Huế
- Đà Nẵng
- Hội An
- Nha Trang
- Đà Lạt
- TP. Hồ Chí Minh
- Cần Thơ
- Hạ Long
Ví dụ trên dùng một <p role="status" className="sr-only"> cập nhật sau khi người dùng ngừng gõ 600ms. Với nhiều thông báo từ nhiều nơi, dùng tiện ích AriaLiveAnnouncer + useAnnounce(): hàng đợi tự giãn cách để không thông báo nào bị nuốt, và batchId thay thông báo cũ cùng loại bằng thông báo mới nhất.
import { AriaLiveAnnouncer, useAnnounce, YugenProvider } from "@/components/ui";
// 1. Gắn một lần ở gốc: render hai vùng live ẩn (polite / assertive)
<YugenProvider applyToDocument>
<Toaster />
<AriaLiveAnnouncer>
<App />
</AriaLiveAnnouncer>
</YugenProvider>
// 2. Gọi ở bất kỳ đâu
function UploadButton({ files }: { files: File[] }) {
const { announce } = useAnnounce();
async function upload() {
announce("Đang tải lên…");
try {
await uploadFiles(files);
announce(`Đã tải lên ${files.length} tệp`, { batchId: "upload" });
} catch {
announce("Tải lên thất bại. Thử lại sau.", { politeness: "assertive", batchId: "upload" });
}
}
return <Button onClick={upload}>Tải lên</Button>;
}Nên & không nên
- Kèm icon và chữ cho mọi trạng thái — không chỉ dựa vào màu.
- Viết thông báo nói rõ chuyện gì đã xảy ra và cần làm gì tiếp theo.
- Một sự kiện — một thông báo; gộp các cập nhật liên tiếp bằng batchId hoặc debounce.
- Giữ toast lỗi hoặc toast có hành động tới khi người dùng đóng (timeout: -1).
- Không dùng role="alert" cho thông tin thường — nó cắt ngang người dùng.
- Không thông báo theo từng phím gõ.
- Không đặt thông tin hay hành động duy nhất trong toast tự đóng.
- Không dùng Dialog cho thông báo không cần quyết định.