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.

Previewindex.htmltext-toolbar.html
B I U
<!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

Each REALM UI component page lists its roles and keyboard support.

Edit this page on GitHub · View as Markdown