HTML & CSS / Maintainable presentation systems / Lesson 17
Accessible disclosure and interaction patterns
Choose native interaction where suitable, keep state and focus coherent, and recognize where HTML/CSS alone cannot implement a complete widget.
On this page
Lesson goals
- An appearance is not an interaction contract
- Style native state without replacing it
- Verify the whole keyboard journey
An appearance is not an interaction contract
A component’s contract includes its role, accessible name, state, keyboard commands, focus movement, and result. A div styled to look like a button does not acquire native button behavior. Adding role="button" supplies a semantic claim but does not implement activation or keyboard handling. Prefer the native element when its behavior matches the task, and use WAI-ARIA patterns as implementation guidance when a custom widget is truly necessary. CSS can style expanded state but cannot alone guarantee correct focus management or state synchronization in an application. Begin by describing the task and choosing the simplest interaction that fulfills it.
Use native disclosure for suitable content
details with a summary provides a native disclosure pattern for additional information. The summary should identify what can be revealed, and essential instructions should not be hidden where the user needs them to begin the task. Native behavior handles expansion and exposes state through the platform, though real browser and assistive-technology testing remains necessary. Do not force a role or aria-expanded onto summary without a specific justified need, since redundant or contradictory semantics can cause problems. A disclosure is not automatically a full application menu, tab interface, or modal dialog; those patterns have different keyboard and focus obligations.
Style native state without replacing it
The first example uses details[open] to change the border and summary styling while leaving expansion to HTML. The summary remains keyboard-focusable under its native behavior, and its focus-visible style is distinct from the open-state border. Clicking or keyboard activation updates the real open attribute, so presentation follows actual state rather than a disconnected class. Do not remove all disclosure indicators unless a clear replacement communicates affordance and state. The example’s content is supplemental travel information, making disclosure a reasonable fit. Keep source order useful if styles fail, and test opening and closing rather than only photographing the open state.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Native disclosure</title>
<style>
details { padding: 1rem; border: 1px solid #555; } details[open] { border-color: #174e9a; } summary { cursor: pointer; } summary:focus-visible { outline: 3px solid #174e9a; outline-offset: 3px; }
</style>
</head>
<body>
<main><h1>Visit the workshop</h1><details><summary>Travel information</summary><p>The entrance is beside the east courtyard.</p></details></main>
</body>
</html>
Travel information can be opened and closed through the summary.
The summary uses native disclosure behavior. The open attribute drives styling, while keyboard focus has a separate visible indicator.
Keep independent disclosures independent
The second example offers two ordinary details elements without forcing mutual exclusion. Users can compare their contents at the same time. Exclusive accordion behavior is a product choice, not an automatic accessibility requirement; it can remove information someone was using. If implementing a custom accordion, synchronize button state with panel visibility, provide the documented keyboard behavior, and preserve focus when panels change. For dialogs, native dialog APIs can help, but modal opening, dismissal, naming, focus placement, and focus restoration require deliberate implementation and testing. A static dialog with open is not equivalent to calling showModal or implementing every modal obligation.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Independent disclosures</title>
<style>
details { margin-block: 1rem; padding: 1rem; border: 1px solid; } summary:focus-visible { outline: 3px solid #174e9a; outline-offset: 3px; }
</style>
</head>
<body>
<main><h1>Workshop questions</h1><details><summary>What should I bring?</summary><p>A text editor and the supplied notes.</p></details><details><summary>Can I review later?</summary><p>The local examples can be saved for practice.</p></details></main>
</body>
</html>
Both answers can be expanded independently.
Each disclosure has its own state and can remain open alongside the other. No script or exclusive accordion assumption is required.
Verify the whole keyboard journey
Tab to the control, identify its name and state, activate it with the expected keys, read the revealed content, and leave the component without a trap. Repeat with zoom, high contrast, and relevant assistive technology. Hidden panels must not retain unexpected interactive descendants in the ordinary focus path. Avoid positive tabindex as a shortcut and do not move focus simply because a CSS animation finished. If motion is decorative, respect reduced-motion preferences without removing information. Automated scans can catch some missing names or attributes, but they cannot prove the complete keyboard contract or the appropriateness of the chosen interaction pattern.
Common errors and fixes
A styled div cannot be activated by keyboard
- Why it happens
- Visual appearance replaced a native control.
- How to fix it
- Use a real button or implement the entire justified custom pattern.
- Prevent it next time
- Test role, name, state, and keys together.
Expanded styling disagrees with actual visibility
- Why it happens
- A CSS class and widget state diverged.
- How to fix it
- Drive presentation from the authoritative state.
- Prevent it next time
- Test repeated open and close operations.
A modal returns focus to nowhere
- Why it happens
- Dismissal lacks a restoration policy.
- How to fix it
- Restore focus to a meaningful surviving control.
- Prevent it next time
- Include dismissal and return paths in tests.
Check your understanding
1. Choose a pattern for optional shipping instructions versus a confirmation that blocks other work.
Show worked solution
A native disclosure can suit optional instructions because content opens inline. A blocking confirmation may require a properly implemented modal dialog with naming, initial focus, dismissal, and restoration. Styling details to resemble a floating modal does not provide the modal interaction contract.
2. What should happen if two FAQ answers are useful for comparison?
Show worked solution
Allow both independent disclosures to remain open unless the product has a justified contrary requirement. Verify their names and keyboard behavior individually. Automatically closing one answer can frustrate comparison and is not required merely to call the component an accordion.
Key takeaways
- Prefer native behavior when it matches the task.
- CSS styling cannot replace widget semantics or focus management.
- Test entry, activation, reading, and exit.