diff --git a/pages/examples/widgets/tablists/README.md b/pages/examples/widgets/tablists/README.md index 078c42c5..2f322a4d 100644 --- a/pages/examples/widgets/tablists/README.md +++ b/pages/examples/widgets/tablists/README.md @@ -5,74 +5,101 @@ position: 6 # Tablist widgets (or: tab panels, tabs) -**Tablists help to split up a page's content into smaller and thus more digestible parts. Each part is minimally represented in a list of names, by which their visibility can be enabled one at a time. Tablists can be thought of as small page fragments inside a page.** +**Tablists are used to divide complex page content into smaller, more manageable sections. Each section is represented by a tab label that allows users to display one panel at a time. In this sense, tablists can be understood as small, self-contained page fragments within a larger page.** [[_TOC_]] Tablists are well known as native controls in many operating systems: a list of controls (usually on top of the element) allows to toggle the visibility of corresponding panels. Only a single control can be active at a time, so exactly one panel is visible and all others are hidden. -![Tablist](_media/tablist.png) +![Tablist](_media/screenshot-of-a-tablist.png) -We do not call tablists simply "tabs" so the difference to the `Tab` key is obvious. +We use the term *tablist* instead of simply *tabs* to avoid confusion with the `Tab` key used for keyboard navigation. + +--- ## General requirements -The following requirements are based on well established best practices and [WAI-ARIA Authoring Practices: Tab Panel Widget](https://www.w3.org/TR/wai-aria-practices/#tabpanel). +The following requirements are based on established best practices and the [WAI-ARIA Authoring Practices: Tab Panel Widget](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/). + +In particular, a compliant tablist must fulfil the following criteria: + +- The purpose and usage of the tablist must be clearly understandable. +- The active and inactive states of tabs must be visually and programmatically perceivable. +- Users must receive clear feedback when a tab is activated. +- The tablist must be fully operable using: + - keyboard only, + - screen readers (desktop and mobile), + - standard interaction keys (`Tab`, `Enter`/`Space`, `Home`/`End`, arrow keys). +- Panel content must be easily accessible via keyboard and assistive technologies. -Besides many other requirements, we want to stress out explicitly the following: +--- + +## Similarities with accordions and carousels -- The meaning and usage of the tablist must be clear. -- The state of each tab control must be perceivable ("active/inactive" or similar). -- Proper feedback must be given upon activating a tab control ("active" or similar). -- The tablist must be operable using both keyboard only and screen readers (with a reasonable interplay of default keys like `Tab`, `Enter`/`Space`, `Esc`, `Arrow` keys), as well as mobile screen readers. -- The panel contents must be easily accessible using both keyboard only and screen reader. +Although tablists, accordions, and carousels appear visually different, they all address the same fundamental use case: controlling the visibility of related content sections. -### Similarities with accordions and carousels +- **Tablists** provide the most basic pattern: one active panel at a time. +- **Carousels** extend this pattern with previous/next controls and optional autoplay. +- **Accordions** stack controls and panels vertically and may allow multiple panels to be open simultaneously. -Maybe you never noticed that tablists, carousels, and accordions - although looking pretty distinct from each other - all solve a very similar use case: toggling the visibility of contents. +Because of these similarities, many of the following principles also apply to: -Tablists are the most basic pattern of them all. Carousels then extend it by providing additional controls like previous/next and autoplay/pause button(s). And accordions extend it by stacking all controls and panels on top of each other; in addition, some of them allow to display multiple panels at the same time. +- [Carousels](/examples/widgets/carousel) +- [Accordions](/examples/widgets/accordion) -Because of this, the following texts apply not only to tablists, but also to carousels (see [Carousels (or: slideshow, slider)](/examples/widgets/carousel)) and accordions (see [Accordions](/examples/widgets/accordion)). +--- ## Proofs of concept -Before you go on, please read [What is a "Proof of concept"?](/examples/widgets/proof-of-concept) (POC). +Before continuing, please read [What is a "Proof of Concept"?](/examples/widgets/proof-of-concept) (POC). + +ARIA-based tablists are well supported across modern browsers and assistive technologies. When native HTML elements cannot express the required behaviour, an ARIA-based implementation is recommended. -ARIA is supported pretty well for tablists (see POC #1). If you need a much simpler solution though, and according to our credo [Widgets simply working for all](/knowledge/semantics/widgets), the easiest way to create a tablist is using a simple group of radio buttons (see POC #2). +--- + +### POC #1: ARIA (Recommended) + +This implementation follows the +[WAI-ARIA Authoring Practices Guide for Tabs](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/) +and uses **manual activation**. Users activate a focused tab using `Enter`, `Space`, or a mouse click. + +Manual activation is recommended when panel content cannot be displayed instantly or when activating a tab triggers expensive operations. -### POC #1: ARIA +The implementation uses appropriate: -TODO +- Roles: `tablist`, `tab`, `tabpanel` +- States: `aria-selected` +- Relationships: `aria-controls`, `aria-labelledby` + +This ensures reliable screen reader support across browsers and platforms. + +[Example](_examples/tablist-with-aria) #### Implementation details -TODO +- Uses the roving tabindex pattern: + - Active tab: `tabindex="0"` + - Inactive tabs: `tabindex="-1"` +- Keyboard interaction: + - `Arrow Left/Right`: Move focus between tabs + - `Home/End`: Move focus to first/last tab + - `Enter/Space`: Activate focused tab + - `Tab`: Move focus out of the tablist +- Tabs are laid out horizontally using flexbox. +- Activation logic ensures that only the selected panel is visible at a time. +- Inactive panels are hidden using the `hidden` attribute to ensure proper support in assistive technologies. +- In some implementations, tab elements use `display: block` (instead of `inline-block`) to improve navigation in NVDA browse mode. +- ARIA attributes establish clear relationships between tabs and panels. -### POC #2: Radio buttons +--- -They can be styled visually as needed using CSS, and spiced up with (very little) JavaScript, so they behave like perfect tablists. +### POC #2: Radio buttons (Legacy) -Sensible naming of elements (and a few specifically added visually hidden texts) guarantees that screen reader users know how to handle the element - even if they have not seen any other tablist before. +**Note:** This approach is deprecated and provided for reference only. -[Example](_examples/tablist-with-radio-buttons) +The ARIA-based implementation (POC #1) should be used for all new projects, as it offers correct semantics and more reliable support for modern assistive technologies. -#### Implementation details +The radio button approach was previously used as a simpler alternative based on native form controls. However, radio buttons represent form input choices rather than navigational relationships between tabs and panels. This semantic mismatch leads to poorer screen reader support and inconsistent interaction patterns, requiring significant workarounds to achieve comparable accessibility. -Some interesting peculiarities: - -- Elements are announced properly by screen readers, and it is clearly perceivable which control is active: the one with the active radio button. -- Proper feedback is given upon interaction: whenever a control is activated, the screen reader announces the respective radio button's state. -- Collapsed panels are hidden effectively from everybody, see [Hiding elements from all devices](/examples/hiding-elements/from-all-devices). -- Where functionality may not be obvious to screen reader users, descriptive text is given (only visible to screen readers): - - The tablist/carousel/accordion's main heading has "tablist/carousel/accordion" appended. - - A small help text explains how the tablist/carousel/accordion works. - - The controls are named "tablist/carousel/accordion controls" and are placed within a `fieldset`/`legend` structure (see [Grouping form controls with fieldset and legend](/examples/forms/grouping-with-fieldset-legend)). - - Each control is named "Show panel X". - - Each panel's heading has "panel" appended. -- The tablist/carousel/accordion controls are placed in the DOM before the panels: - - As the whole element is properly marked up with headings, screen reader users can jump very quickly between controls and panels (see [How to handle headings](/examples/headings/handling)). -- Using `.tablist:focus-within .control label`, a style can be applied to all radio button labels upon interacting with the tablist. - - This gives users a clue that they are interacting with a single control now (indicating to use the `Arrow` keys instead of `Tab` to navigate through tab items). - - If you would rather like to make each control focusable on its own, you could use a group of checkboxes instead of radio buttons. - - Do not forget to make sure only one of them is checked at a time though (using some JavaScript). +[Example](_examples/tablist-with-radio-buttons) +*(Legacy — for reference only)* diff --git a/pages/examples/widgets/tablists/_examples/tablist-with-aria/README.md b/pages/examples/widgets/tablists/_examples/tablist-with-aria/README.md new file mode 100644 index 00000000..487e12a1 --- /dev/null +++ b/pages/examples/widgets/tablists/_examples/tablist-with-aria/README.md @@ -0,0 +1,18 @@ +--- +title: "Tablist with ARIA" +compatibility: + Keyboard only: + status: pass + date: 2026-02-05 + NVDA: + 2023.1 + FF 115: + status: pass + date: 2026-02-05 + 2023.1 + Edge: + status: pass + date: 2026-02-05 + JAWS: + 2023.23 + Edge: + status: pass + date: 2026-02-05 +--- diff --git a/pages/examples/widgets/tablists/_examples/tablist-with-aria/_example.png b/pages/examples/widgets/tablists/_examples/tablist-with-aria/_example.png new file mode 100644 index 00000000..375d9ed3 Binary files /dev/null and b/pages/examples/widgets/tablists/_examples/tablist-with-aria/_example.png differ diff --git a/pages/examples/widgets/tablists/_examples/tablist-with-aria/example.css b/pages/examples/widgets/tablists/_examples/tablist-with-aria/example.css new file mode 100644 index 00000000..1f9e70e2 --- /dev/null +++ b/pages/examples/widgets/tablists/_examples/tablist-with-aria/example.css @@ -0,0 +1,49 @@ +body { + padding: 20px; + font-family: system-ui, sans-serif; +} + +.tablist { + display: flex; + border-bottom: 1px solid #000; +} + +[role="tab"] { + appearance: none; + background: none; + border: 1px solid #000; + border-bottom: none; + padding: 6px 12px; + margin: 0 2px 0 0; + cursor: pointer; +} + +[role="tab"][aria-selected="true"] { + background: lightyellow; + font-weight: 600; +} + +[role="tab"]:hover { + text-decoration: underline; +} + +[role="tab"]:focus-visible { + outline: 2px dotted; + outline-offset: 2px; +} + +[role="tabpanel"] { + border: 1px solid #000; + padding: 1rem; + background: lightyellow; +} + +[role="tabpanel"][hidden] { + display: none; +} + +a:focus-visible, +button:focus-visible { + outline: 2px dotted; + outline-offset: 2px; +} diff --git a/pages/examples/widgets/tablists/_examples/tablist-with-aria/example.js b/pages/examples/widgets/tablists/_examples/tablist-with-aria/example.js new file mode 100644 index 00000000..589fad10 --- /dev/null +++ b/pages/examples/widgets/tablists/_examples/tablist-with-aria/example.js @@ -0,0 +1,77 @@ +'use strict' + +/* + * Tabs (Manual Activation) + * WAI-ARIA Authoring Practices compliant + * https://www.w3.org/WAI/ARIA/apg/patterns/tabs/ + */ + +class TabList { + constructor(tablist) { + this.tablist = tablist + + this.tabs = [...tablist.querySelectorAll('[role="tab"]')] + + this.panels = this.tabs.map(tab => + document.getElementById(tab.getAttribute('aria-controls')) + ) + + this.init() + } + + init() { + this.tabs.forEach((tab, i) => { + tab.tabIndex = -1 + tab.setAttribute('aria-selected', 'false') + + tab.addEventListener('click', () => this.activate(i)) + tab.addEventListener('keydown', e => this.onKey(e, i)) + }) + + this.activate(0, false) + } + + activate(i, focus = true) { + this.tabs.forEach((tab, n) => { + const active = i === n + + tab.tabIndex = active ? 0 : -1 + tab.setAttribute('aria-selected', active) + + this.panels[n].toggleAttribute('hidden', !active) + }) + + if (focus) this.tabs[i].focus() + } + + focus(i) { + const max = this.tabs.length - 1 + + if (i < 0) i = max + if (i > max) i = 0 + + this.tabs[i].focus() + } + + onKey(e, i) { + const keys = { + ArrowLeft: () => this.focus(i - 1), + ArrowRight: () => this.focus(i + 1), + Home: () => this.focus(0), + End: () => this.focus(this.tabs.length - 1), + Enter: () => this.activate(i), + ' ': () => this.activate(i) + } + + if (!keys[e.key]) return + + keys[e.key]() + e.preventDefault() + } +} + +/* Init */ + +window.addEventListener('DOMContentLoaded', () => { + document.querySelectorAll('[role="tablist"]').forEach(el => new TabList(el)) +}) diff --git a/pages/examples/widgets/tablists/_examples/tablist-with-aria/index.html b/pages/examples/widgets/tablists/_examples/tablist-with-aria/index.html new file mode 100644 index 00000000..87a09fe5 --- /dev/null +++ b/pages/examples/widgets/tablists/_examples/tablist-with-aria/index.html @@ -0,0 +1,47 @@ +

+ +

+ +
+

Flowers

+ +
+ + + +
+ +
+

Rose

+ +

A rose is a woody perennial flowering plant of the genus Rosa.

+ +

+ Learn more about roses +

+
+ + + + +
+ +

+ +

diff --git a/pages/examples/widgets/tablists/_examples/tablist-with-radio-buttons/README.md b/pages/examples/widgets/tablists/_examples/tablist-with-radio-buttons/README.md index 13eada48..d9ac2a24 100644 --- a/pages/examples/widgets/tablists/_examples/tablist-with-radio-buttons/README.md +++ b/pages/examples/widgets/tablists/_examples/tablist-with-radio-buttons/README.md @@ -1,5 +1,5 @@ --- -title: "Tablist with radio buttons" +title: "Tablist with radio buttons (Legacy)" compatibility: Keyboard only: status: pass diff --git a/pages/examples/widgets/tablists/_examples/tablist-with-radio-buttons/_example.png b/pages/examples/widgets/tablists/_examples/tablist-with-radio-buttons/_example.png index 15367d17..375d9ed3 100644 Binary files a/pages/examples/widgets/tablists/_examples/tablist-with-radio-buttons/_example.png and b/pages/examples/widgets/tablists/_examples/tablist-with-radio-buttons/_example.png differ diff --git a/pages/examples/widgets/tablists/_examples/tablist-with-radio-buttons/example.css b/pages/examples/widgets/tablists/_examples/tablist-with-radio-buttons/example.css index 853a0b32..7a1f7d4c 100644 --- a/pages/examples/widgets/tablists/_examples/tablist-with-radio-buttons/example.css +++ b/pages/examples/widgets/tablists/_examples/tablist-with-radio-buttons/example.css @@ -9,45 +9,56 @@ body { padding: 20px; + font-family: system-ui, sans-serif; } fieldset { border: 0; - padding: 4px; + padding: 0; } -.tablist:focus-within .control label { - background-color: yellow; +.controls { + display: flex; + border-bottom: 1px solid #000; } .control { display: inline-block; } + .control label { - border: 1px solid black; - margin: 0 0 -1px 0; - padding: 4px 10px; + appearance: none; + background: none; + border: 1px solid #000; + border-bottom: none; + padding: 6px 12px; + margin: 0 2px 0 0; + cursor: pointer; + display: block; } input[type="radio"]:checked + label { - background-color: lightyellow !important; - border-bottom-color: lightyellow; + background: lightyellow; + font-weight: 600; +} + +.control label:hover { + text-decoration: underline; } -a:focus, -input:focus, -input[type="radio"]:focus + label { +input[type="radio"]:focus-visible + label { outline: 2px dotted; outline-offset: 2px; } -input[type="radio"] + label:hover { - cursor: pointer; - text-decoration: underline; +.panel { + border: 1px solid #000; + padding: 1rem; + background: lightyellow; } -.panel { - border: 1px solid; - background-color: lightyellow; - padding: 0 0 0 10px; +a:focus-visible, +button:focus-visible { + outline: 2px dotted; + outline-offset: 2px; } diff --git a/pages/examples/widgets/tablists/_examples/tablist-with-radio-buttons/index.html b/pages/examples/widgets/tablists/_examples/tablist-with-radio-buttons/index.html index 098929a1..17950c98 100644 --- a/pages/examples/widgets/tablists/_examples/tablist-with-radio-buttons/index.html +++ b/pages/examples/widgets/tablists/_examples/tablist-with-radio-buttons/index.html @@ -1,3 +1,6 @@ +

+ Deprecated: Use ARIA-based implementation instead. Radio buttons are unsuitable for tablists because they represent form input choices rather than navigational relationships between tabs and panels, leading to poorer screen reader support and inconsistent interaction. +

@@ -11,82 +14,49 @@

Tablist controls
- +
- +
- +

rose (panel)

-

- Some info about rose -

-

- Bla bla bla... all about rose... -

-

- A beautiful rose -

+

Rose

+ +

A rose is a woody perennial flowering plant of the genus Rosa.

+

- A link to a page with more infos about rose! -

-

- More elements can come here related to rose -

-

- Maybe even some form element where you can enter a name for your rose: + Learn more about roses

diff --git a/pages/examples/widgets/tablists/_media/screenshot-of-a-tablist.png b/pages/examples/widgets/tablists/_media/screenshot-of-a-tablist.png new file mode 100644 index 00000000..375d9ed3 Binary files /dev/null and b/pages/examples/widgets/tablists/_media/screenshot-of-a-tablist.png differ diff --git a/pages/knowledge/semantics/widgets/README.md b/pages/knowledge/semantics/widgets/README.md index a93097d0..99ea8011 100644 --- a/pages/knowledge/semantics/widgets/README.md +++ b/pages/knowledge/semantics/widgets/README.md @@ -5,46 +5,46 @@ position: 3 # Widgets simply working for all -**HTML supports interactive controls for most requirements. But what about additional interaction patterns that do not offer an HTML equivalent? Surprisingly to many, standard browser behaviour is also a fool-proof way to provide even complex custom functionalities in the style of modern widgets. The trick is to simply use traditional form controls, change their visual design using CSS, and add the needed interactivity using JavaScript.** +**HTML supports interactive controls for most requirements. But what about additional interaction patterns that do not offer an HTML equivalent? The key is to use semantic HTML and standard controls where possible, enhance them with ARIA when needed, style them with CSS, and add interactivity with JavaScript. This approach leverages native browser behavior and ensures widgets work for everyone.** [[_TOC_]] -Let's think about the true spirit of a typical widget, for example a tablist: what is its purpose? It offers a list of items that are toggling the visibility of related containers. Only one item can be visible at a time, and if another one is activated, the previously active one gets deactivated automatically. +## The principle -This sounds a lot like a group of radio buttons, does it not? +When creating custom widgets, start with semantic HTML and standard form controls. These provide built-in accessibility features, keyboard support, and screen reader compatibility. Then: -## Tablist using radio buttons +1. **Enhance with ARIA** when semantic HTML alone isn't sufficient + - **Important:** Pay attention to the robustness of ARIA roles and attributes used, as browsers and screen readers still differ quite a lot in how certain ARIA roles and attributes are interpreted. Always test with multiple assistive technology combinations. +2. **Style with CSS** to achieve the desired visual design +3. **Add interactivity with JavaScript** to create the widget behavior -Instead of re-inventing the wheel by creating our own tablist implementation using lots of more or less meaningful custom HTML containers and lots of JavaScript, why not simply re-use existing standard behaviour? +This approach ensures that widgets work out of the box for keyboard users and screen reader users, while still allowing complete visual customization. -```html -
-
- Tablist controls - - +## Using ARIA for complex widgets - - -
+For widgets that don't have a direct HTML equivalent (like tablists, accordions, or carousels), ARIA provides the necessary semantic structure. ARIA roles, states, and properties communicate the widget's structure and behavior to assistive technologies. -
...
- -
-``` +**Note on robustness:** When using ARIA, be aware that browser and screen reader support varies. Different combinations interpret ARIA roles and attributes differently. Always test your implementation with multiple assistive technology combinations to ensure robust accessibility across different platforms. -With this approach, we only need to react on the change of a radio button group using JavaScript and toggle the visibility of the respective tabpanel. Everything else works perfectly out of the box, also for keyboard only and screen reader users. And sure, we can change the visual properties to anything we like to resemble a tablist. +### Example: Tablists -## Other user interface patterns +A tablist allows users to toggle the visibility of content panels. The recommended approach is to use ARIA roles and attributes following the [WAI-ARIA Authoring Practices Guide](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/). This provides: -It is not always as easy as with tablists. But the approach of using standard HTML form controls to mimic modern control patterns can be applied to most requirements. In fact, this approach is the base of many complex interactive code examples in this guide. If you are really curious and want to learn more about this, skip ahead and read [Interactive widgets](/examples/widgets). +- Proper semantic structure that screen readers understand +- Robust keyboard navigation with focus management +- Clear communication of relationships between tabs and panels +- Accurate announcement of the active state -## But isn't this wrong? +For detailed information and implementation examples, see [Tablists](/examples/widgets/tablists). -It may feel a bit weird (if not to say: blasphemic) to use radio buttons as a tablist. But actually, the majority of users (those with good vision) will never even become aware of the radio buttons, as these are acting completely behind the scenes, and it "just works", out of the box, with minimal effort. So in the end, this is a huge plus in many aspects, be it usability, accessibility, readability of code (and thus maintainability), or performance. +## Best practices -For screen reader users, this approach may be a bit surprising in the first place. They might wonder: "Why is there a group of radio buttons? I'm not inside a form, am I?" To alleviate this, we can make the element more self-explanatory by improving the label texts for screen readers, ie. "Show panel Dancing" (instead of just "Dancing"). And as radio buttons are not tied to a `
` element, they can exist anywhere in a website anyway. In general we can say: screen reader users are so much used to encounter inaccessible widgets on a daily basis that they will be very happy that our implementation just does the job for them. - -## ARIA - Pushing accessibility to the max +Regardless of which approach you choose: -If you want to provide an even more accessible experience to screen reader users, you are welcome to use the Accessible Rich Internet Application (ARIA) standard which is aimed to implement widgets that are 100% optimised for screen readers. Just read on, please. +- **Start with semantic HTML** - Use the most appropriate HTML element for the job +- **Enhance with ARIA when needed** - Add ARIA roles, states, and properties for complex widgets +- **Ensure keyboard accessibility** - All interactive elements must be keyboard accessible +- **Test with assistive technologies** - Verify that screen readers announce widgets correctly +- **Follow established patterns** - Use the [WAI-ARIA Authoring Practices Guide](https://www.w3.org/WAI/ARIA/apg/) as a reference + +For more examples of accessible widgets, see [Interactive widgets](/examples/widgets).