Accessible Multilingual Interface and Composite Controls
Practical 02 - Accessible Multilingual Interface and Composite Controls
Related: Chapter 2 · Lecture slides
Objective
Build a resilient, fully accessible public-service portal that demonstrates the platform relationship between semantic HTML, the accessibility tree, internationalization, and live DOM event handling.
You will establish a native selection baseline first using standard HTML controls, prove that the interface is completely operable via keyboard with explicit labeling and validation, handle mixed English and Kurdish/Arabic directional text, and implement delegated DOM events. As an optional extension, you will implement an accessible composite widget (a custom multi-select listbox) with roving tabindex.
Prerequisites and setup
You need basic HTML, CSS, JavaScript, and a browser with developer and accessibility inspection tools (e.g. Chrome/Firefox Accessibility Tree inspection panel). Serve the exercise locally using any local static HTTP server (e.g., python -m http.server 8000 or npx serve .):
Create this folder structure:
Ensure your stylesheet includes a distinct, high-contrast :focus-visible outline. Keep third-party UI libraries or CSS frameworks out of the project; all behaviors and styles must be native.
Stage 1 - Establish semantic landmarks and native selection baseline
Construct the core portal page containing:
- A
<header>with site title and a<nav aria-label="Primary">containing navigation links. - A single
<main>landmark with a logical heading structure (<h1>$\rightarrow$<h2>). - A service request form using native interactive controls:
- A native
<select id="service-type" name="service" required>dropdown with options for different certificate requests. - A
<fieldset>with a<legend>containing radio buttons for submission urgency (Standard vs Express). - A
<button type="submit">element.
- A native
- A recent-requests
<table>with a<caption>, explicit header scopes (<th scope="col">and<th scope="row">), and sample rows. - A
<footer>containing organizational metadata.
Verify: Disable CSS in your browser. Verify that the document outline, landmarks, form controls, and table data remain immediately understandable and navigable.
Stage 2 - Programmatic labeling, keyboard operability, and error association
Enhance the form with comprehensive accessibility associations:
- Ensure every control has an explicit
<label for="...">matching the input’sid. - Add a multiline
<textarea id="notes" name="notes" required>for applicant statements. - Configure explicit error association: add a dedicated
<p id="notes-error" role="alert" hidden></p>element. - Connect the error container to the input via
aria-describedby="notes-error notes-hint". - In
app.js, intercept form submission. If the textarea is empty, markaria-invalid="true", populate the error text, unhide the container, and programmatically move focus to the invalid field.
Verify: Navigate the entire page using only the Tab, Shift+Tab, Space, and Enter keys. Confirm that:
- Every control receives a visible focus indicator.
- No interactive element requires a mouse to activate.
- Form validation errors are programmatically announced by screen-reader tools when triggered.
Stage 3 - Multilingual content, directionality, and event delegation
Introduce internationalization and live DOM manipulation:
- Declare the primary document language on
<html>(lang="en" dir="ltr"). - On the notes field, configure
dir="auto"so citizen statements written in Central Kurdish (ckb) or Arabic (ar) automatically align right-to-left. - In the recent requests table, render multilingual rows. Use
<bdi>to isolate user-submitted RTL statements so they do not scramble adjacent reference codes or punctuation. - In
app.js, implement a dynamic row addition feature upon form submission. Use safe DOM methods (document.createElement,textContent,append) rather than rawinnerHTML. - Add an action button (
<button type="button" data-action="cancel">) to each table row. Instead of attaching a listener to every button, attach one listener to the<tbody>element and useevent.target.closest('button[data-action="cancel"]')to handle cancellations via event bubbling. - Expose dynamic updates to assistive devices using a live region (
role="status"andaria-live="polite").
Verify: Add an entry containing Arabic or Kurdish text alongside English IDs. Confirm that text direction is properly isolated and that clicking dynamic buttons triggers the delegated handler correctly.
Stage 4 (Optional Extension) - Composite multi-select listbox
Only after completing Stages 1–3, evaluate whether a custom widget is justified. Build an enhanced multi-select listbox:
- Create a container with
role="listbox",aria-multiselectable="true", andaria-label="Available services". - Inside, render child options with
role="option"andaria-selected="false". - Implement roving
tabindex:- The currently focused option has
tabindex="0"; all other options havetabindex="-1". - Pressing
DownArroworUpArrowmoves focus and updatestabindex="0"to the next or previous option without scrolling the page. Homemoves focus to the first option;Endmoves to the last option.Spacetoggles the selection state (aria-selected="true|false").
- The currently focused option has
- Preserve the native
<select multiple>as a fallback.
Verify: Inspect the accessibility tree in DevTools. Verify that the custom listbox exposes correct roles, options, and selection states, and that navigating via Tab treats the listbox as a single tab stop.
What to submit
Document your implementation by submitting the completed project files (index.html, styles.css, app.js) and filling out this verification table:
| Verification Target | Requirement | Observed Evidence | Explanation | Confounders or Limits |
|---|---|---|---|---|
| Document Hierarchy | Logical h1-h2 structure and landmarks | Accessibility tree snapshot | Document outline exposes clean landmarks | Checked in Chrome/Firefox DevTools |
| Keyboard Navigation | 100% operable without mouse; visible focus | Tab progression sequence | :focus-visible styling provides clear indicator | Verified with keyboard only |
| Form Validation | Error announced via aria-describedby | Accessibility tree inspection | aria-invalid="true" set upon empty submit | Browser native validation disabled for test |
| Internationalization | <bdi> isolates mixed LTR/RTL content | Visual alignment test | Text punctuation remains intact across scripts | Tested with Central Kurdish / Arabic input |
| Event Delegation | Single listener handles dynamic rows | Console event logs | event.target.closest() captures row action | Event bubbling through <tbody> |
When an experiment gives an unexpected result
- No focus ring visible: Verify that your CSS does not contain
outline: nonewithout a matching:focus-visibledeclaration. - Screen reader does not announce validation error: Check that the error container has
role="alert"oraria-live="assertive", and thataria-describedbymatches the error element’s exactid. - RTL text scrambles adjacent numbers: Confirm that the user string is wrapped inside
<bdi>or hasdir="auto". - Delegated click listener does not fire: Check whether
event.target.closest(...)selector matches the button, and ensure the event is allowed to bubble (i.e.stopPropagation()was not called on a child element).