Accessibility
REALM components are native elements with shadow roots, so the usual rules apply: use native <button>, <a>, <input> and <dialog> elements inside, keep a logical focus order, and expose state with ARIA. REALM adds the helpers below.
Default roles
role on the definition gives every instance a default role through ElementInternals. Pages can still override it with a role attribute on the element.
<custom-element name="status-pill" role="status">
<slot></slot>
</custom-element>Because the role lives in ElementInternals, it does not appear as an attribute. Some testing tools, including older versions of axe-core, do not read it.
Delegating focus
delegates-focus on the definition creates the shadow root with delegatesFocus: true. Focusing the host, clicking it, or pointing a <label> at it moves focus to the first focusable element inside, and the host matches :focus:
<custom-element name="search-box" delegates-focus>
<input type="search" aria-label="Search">
</custom-element>Every REALM UI control that wraps a native control delegates focus.
Arrow-key navigation with focusgroup
Toolbars, menus, tab lists and radio groups move focus with the arrow keys instead of Tab. Add a focusgroup attribute to the container in a component's markup:
<div role="toolbar" aria-label="Formatting" focusgroup="toolbar inline wrap">
<button>Bold</button>
<button>Italic</button>
<button>Underline</button>
</div>REALM implements the arrow-key behaviour of the Open UI focusgroup proposal in every browser, including those without native support:
| Token | Effect |
|---|---|
inline |
Only Left and Right arrows move focus (mirrored in right-to-left text) |
block |
Only Up and Down arrows move focus |
| neither | All four arrows move focus |
wrap |
Moving past the last item goes to the first, and the other way round |
Home and End move to the first and last item. Items are the focusable descendants (buttons, links with href, form controls, elements with tabindex, and custom elements that delegate focus), including content slotted in from outside. Disabled, hidden, inert and aria-disabled="true" items are skipped, and so is anything with tabindex="-2". The first token describes the widget (toolbar, menu, tablist, listbox, …) and is there for readability.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Accessibility demo</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/toggle.html"></import-element>
<import-element from="./text-toolbar.html"></import-element>
<text-toolbar></text-toolbar>
</body>
</html><custom-element name="text-toolbar">
<style>
div {
display: inline-flex;
gap: calc(var(--spacing) * 1);
padding: calc(var(--spacing) * 1);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
}
</style>
<div role="toolbar" aria-label="Formatting" focusgroup="toolbar inline wrap">
<ui-toggle aria-label="Bold"><b>B</b></ui-toggle>
<ui-toggle aria-label="Italic"><i>I</i></ui-toggle>
<ui-toggle aria-label="Underline"><u>U</u></ui-toggle>
</div>
</custom-element>Click the first button, then use the Left and Right arrows, Home and End.
The focusgroup moves focus. It does not manage tabindex. When a widget should be a single Tab stop, set tabindex="-1" on the items that are not current, as REALM UI's tabs and calendar do.
ARIA state from data
Bind ARIA attributes like any other attribute. Booleans become "true" and "false", which is what ARIA expects:
<button _aria-expanded="#menu.open" aria-controls="menu" onclick="toggle()">Menu</button>
<li repeat="#list.items" as="item" key="id" role="option" _aria-selected="#item.selected">…</li>
<section _aria-busy="#feed.loading">…</section>Elements with fetch attributes get aria-busy="true" automatically while their request runs.
Labelling form fields
For form-associated components that delegate to an inner control with ref="control", a native <label> around the component, or one whose for points at it, names the inner control. Without a label, pass aria-label through to the control with a prop:
defineProps({ label: prop.string().attribute("aria-label") });<input ref="control" _aria-label="@label">Content that appears and disappears
<is-visible> and <is-hidden> remove content from the DOM, so assistive technology does not see hidden content. When content changes in place and users should hear it, put it in a live region (role="status" or aria-live="polite") that stays in the DOM.
Checklist for your own components
- Use native interactive elements inside. Never make a
<div>clickable. - Give icon-only buttons an accessible name.
- Show focus: style
:focus-visible(REALM UI usesbox-shadow: var(--focus-ring)). - Respect
prefers-reduced-motionfor animations. - Test with the keyboard only, and with a screen reader.
Each REALM UI component page lists its roles and keyboard support.