Một timeline có thể nhìn rất ổn: đường dọc rõ, chấm mốc đều, ngày tháng nổi bật và mỗi sự kiện nằm trong một card. Nhưng với screen reader, phần trang trí đó gần như không mang thông tin. Nếu HTML chỉ là một chuỗi div, người dùng phải tự đoán đâu là một mốc, đang ở vị trí nào và nội dung mới vừa xuất hiện hay chưa.
Câu hỏi thực tế không phải “gắn ARIA nào cho timeline?”, mà là timeline này đang hoạt động như một danh sách hữu hạn hay một feed tự tải. Phần lớn trường hợp nên bắt đầu bằng ol, li, article và nút tải thêm. role="feed" chỉ đáng dùng khi sản phẩm thực hiện được cả hợp đồng focus, tải dữ liệu và đường thoát cho người đọc.
Mục lục [Hiển thị]
Đường kẻ không tạo ra cấu trúc cho screen reader
CSS có thể vẽ quan hệ thời gian bằng ::before, màu và khoảng cách. Accessibility API lại cần cấu trúc khác: tập hợp này có bao nhiêu phần tử, mỗi phần tử tên gì, thứ tự ra sao và hành động nào thuộc về nó. Hai lớp có thể dùng chung DOM, nhưng không tự sinh ra nhau.
Với lịch sử giao dịch, changelog hoặc activity log hữu hạn, ordered list thường là điểm bắt đầu hợp lý. Thứ tự là một phần ý nghĩa của dữ liệu, nên ol truyền được nhiều hơn một dãy card trung tính. Bên trong mỗi li, có thể dùng heading cho tên sự kiện, time datetime="…" cho thời điểm máy đọc được và một đoạn mô tả ngắn.
<ol class="timeline">
<li>
<h3>Mở vòng audit</h3>
<time datetime="2026-10-12T10:15:00+07:00">12/10, 10:15</time>
<p>Kiểm tra keyboard và screen reader.</p>
</li>
</ol>
Đừng thêm role="list" và role="listitem" chỉ để HTML trông “accessible” hơn. Semantics native đã đủ thì giữ nguyên. Chỉ khôi phục role khi một lựa chọn CSS hoặc component framework thật sự làm semantics biến mất trong tổ hợp browser và assistive technology cần hỗ trợ.

Fixture A/B dùng cùng ba sự kiện. Cột A là ol/li với nút tải thêm; cột B là feed gồm các article có focus. Sự khác biệt nằm ở hành vi, không nằm ở việc card bên nào đẹp hơn.
Timeline tĩnh nên bắt đầu bằng list
Trong lượt kiểm tra ngày 15/10/2026, fixture chạy trên Windows 11 26H2 build 26300.9457, Chrome 155.0.8059.41 và NVDA 2026.2 portable, không add-on. Đây là bài test cục bộ, không phải chứng nhận tương thích cho mọi browser hay screen reader.
Từ đầu trang, phím H đưa NVDA tới heading “A. Danh sách có nút tải thêm”. Phím L đọc “list, with 3 items”, sau đó là heading “Khởi tạo dự án”. Nghĩa là người đọc có hai đường quét hữu ích: theo heading để xem nhanh nội dung, hoặc theo list để hiểu đây là một tập hợp có thứ tự.
Khi nhảy tới nút “Tải thêm 2 sự kiện” và nhấn Enter, fixture thêm hai li. Focus vẫn ở nút, còn vùng status đã tồn tại từ trước thông báo “Đã tải thêm 2 sự kiện. Tổng cộng 5 sự kiện.” Đây là hành vi khá dễ dự đoán: người dùng chủ động yêu cầu dữ liệu, nhận phản hồi và không bị kéo vào một card mới.
Với timeline có điểm kết thúc, phân trang hoặc nút tải thêm, list giải quyết được phần lớn nhu cầu mà không tạo thêm keyboard contract. Nó cũng cho phép bookmark URL theo trang và đưa người dùng tới nội dung sau timeline dễ hơn. Nếu đang audit phân cấp heading của toàn trang, bài Phân cấp thông tin UI: Audit bằng outline và tác vụ quét là bước nối hợp lý trước khi đi sâu vào nội dung động.
Khi nào role="feed" mới đáng dùng?
W3C APG Feed Pattern mô tả feed là một cấu trúc tự tải article khi người dùng đọc hoặc cuộn. Đây không phải widget desktop có keyboard convention phổ quát. Điểm quan trọng hơn cái role là “hợp đồng” giữa trang và assistive technology: DOM focus phải theo article đang được đọc, còn trang chịu trách nhiệm tải, loại và cuộn article theo focus đó.
Mỗi đơn vị trong feed là một article có nhãn phân biệt. Khi DOM chỉ giữ một cửa sổ dữ liệu thay vì toàn bộ tập hợp, aria-posinset cho biết vị trí logic và aria-setsize cho biết tổng. Nếu tổng chưa biết hoặc thay đổi liên tục, WAI-ARIA 1.2 cho phép dùng -1.
<div role="feed" aria-labelledby="timeline-title" aria-busy="false">
<article tabindex="0"
aria-labelledby="event-24"
aria-posinset="24"
aria-setsize="-1">
<h3 id="event-24">Hoàn tất kiểm thử</h3>
…
</article>
</div>
Trong browser test, feed bắt đầu với ba article, aria-busy="false" và mọi article dùng aria-setsize="-1". Handler Page Down của fixture chuyển focus từ article 1 sang article 2 và cập nhật vị trí thành 2. Tuy nhiên, lượt tự động hóa NVDA cho riêng phím Page Down không tạo được bằng chứng sạch đủ để kết luận, nên kết quả này chỉ xác nhận logic DOM. Bài không coi keyboard convention của feed là đã “pass screen reader”.
Ví dụ infinite feed của APG cũng ghi rõ đây là ví dụ minh họa, không phải code production, và có thể có khoảng trống hỗ trợ giữa browser với assistive technology. Nếu đội ngũ không thể kiểm thử các tổ hợp thật, nút tải thêm vẫn là lựa chọn an toàn hơn.

Tải thêm mà không cướp focus
Khi một lần tải cần nhiều thao tác DOM, feed nên đặt aria-busy="true" trước khi sửa và trả về false ngay khi cây đã ổn định. Quên bước cuối có thể khiến thay đổi không được trình bày đúng cho một số assistive technology. Trong fixture, log xác nhận thứ tự là bật busy, thêm hai article, tắt busy, rồi cập nhật status.
Vùng thông báo nên tồn tại trước thao tác:
<p id="timeline-status" role="status" aria-atomic="true"></p>
Sau khi tải xong, JavaScript chỉ thay nội dung vùng này. WCAG Technique ARIA22 giải thích role="status" có live region kiểu polite; người dùng nhận thông báo mà focus không phải nhảy tới đoạn text. aria-atomic="true" được ghi rõ vì một số môi trường không xử lý tính atomic mặc định giống nhau.
Đừng đặt aria-live lên cả timeline. Mỗi lần thêm nhiều card, screen reader có thể đọc một khối dài và lặp lại nội dung người dùng chưa yêu cầu. Cũng không dùng alert cho cập nhật bình thường: “đã tải thêm” là status, không phải tình huống khẩn cấp.

Browser test cho thấy sau khi feed tăng từ ba lên năm article, aria-busy đã về false, status có câu ngắn và active element vẫn là nút tải. NVDA đọc đúng status đó trong lượt kiểm tra sạch. Cách giữ ngữ cảnh này cùng họ với error summary trong form: nội dung thay đổi nhưng người dùng vẫn biết mình đang ở đâu. Bài Form báo lỗi dễ sửa: Error summary, focus và giữ dữ liệu có thêm ví dụ về quyết định khi nào nên giữ focus và khi nào cần chuyển.
Virtualization là chỗ feed dễ vỡ nhất
Infinite scroll thường loại item cũ khỏi DOM để giữ hiệu năng. Nếu article đang có focus bị xóa, người dùng có thể mất vị trí đọc, quay về đầu trang hoặc rơi vào body. Chỉ điền aria-posinset không giải quyết được việc đó; component phải giữ focus ở một phần tử còn tồn tại và ánh xạ đúng vị trí logic.
Feed cũng cần đường thoát. APG đề xuất Ctrl+End tới phần tử focus được sau feed và Ctrl+Home quay về đầu, nhưng đây là convention gợi ý, không phổ quát. Hướng dẫn thao tác phải dễ tìm, còn link hoặc control sau feed phải thật sự đi tới được. Nếu người dùng không thể đến footer vì nội dung cứ tự sinh, thiết kế đã tạo một cái bẫy dù từng article có đủ ARIA.
Back/Forward và deep link cũng cần được tính. Một người mở event số 80 rồi quay lại không nên bị ném về event số 1. Có thể lưu cursor vào URL, giữ anchor ổn định hoặc dùng phân trang. Nếu chi phí khôi phục ngữ cảnh quá lớn, đó là dấu hiệu feed không hợp với timeline này.
Ma trận test nên tách semantics khỏi khả năng dùng
| Task | List + nút tải | Feed tự tải | Bằng chứng cần giữ |
|---|---|---|---|
| Hiểu cấu trúc ban đầu | List count, heading, time | Feed name, article label, vị trí | Speech Viewer và accessibility tree |
| Tải thêm | Focus ở nút, status ngắn | Busy đúng vòng đời, focus không mất | Active element, log state và lời đọc |
| Đi giữa item | Heading/list navigation | Article navigation hoặc key đã tài liệu hóa | Vị trí trước/sau và tên article |
| Thoát timeline | Tab tới nội dung kế tiếp | Control sau feed, Ctrl+End nếu có | Focus đích và scroll position |
| Khôi phục trang | URL/trang hiện tại | Cursor, anchor hoặc event ID | Reload và Back/Forward |
Axe hoặc static checker có thể bắt thiếu accessible name, role sai và một số quan hệ hỏng. Chúng không chứng minh người dùng có thể đọc tới cuối feed, giữ vị trí sau virtualization hoặc hiểu live announcement. Kết quả tự động chỉ là lớp đầu; cần keyboard test và screen reader task trên browser/AT mà sản phẩm thực sự hỗ trợ.
Khi focus đã đi vào article hoặc control, chỉ báo focus cũng phải nhìn thấy. Bài Focus indicator WCAG 2.2: Đo diện tích và tương phản đi sâu vào phần đo thay vì chỉ nhìn cảm giác.
Kết luận: native trước, feed sau khi có bằng chứng
Timeline tĩnh, lịch sử hữu hạn hoặc danh sách tải theo yêu cầu nên dùng HTML native. Cấu trúc dễ đọc, hành vi dễ dự đoán và ít tạo thêm nghĩa vụ cho focus. Feed chỉ phù hợp khi nội dung thật sự tự tải hoặc virtualize, đồng thời đội ngũ kiểm soát được article focus, vị trí logic, aria-busy, status và đường thoát.
Fixture lần này xác nhận list count và status bằng NVDA, đồng thời xác nhận state/focus của feed ở lớp browser. Nó chưa đủ để tuyên bố feed đạt WCAG hay Page Down tương thích mọi screen reader. Đây chính là giới hạn nên giữ khi triển khai thật: nếu chưa test được hợp đồng của feed, quay về nút tải thêm là quyết định tốt, không phải bước lùi.
Nguồn chuẩn để đối chiếu thêm là Understanding WCAG 2.2 SC 4.1.3. Tài liệu này giúp phân biệt status message với thay đổi ngữ cảnh, nhờ vậy tránh dùng live region để chữa mọi vấn đề focus.