Accessibility
What every element does for you, and what your page still has to do.
Every element has a role and a name, one keyboard model, one focus treatment, motion that stops on request, and the system palette under forced colors. Every element meets WCAG 2.1 AA in the default theme, in every shipped preset, light and dark, and in every theme the engine derives: text reads at least 4.5:1 on its ground (success criterion 1.4.3) and the edge of a control at least 3:1 (1.4.11). Contrast has the figures. Each component page carries that element's keyboard table above its API, and every built-in name (Close, Clear, Loading) can be translated: see Localisation. What the elements guard against, and what stays with your page, is on Security.
A composite (a list, a grid, a menu, a tab bar, a tree) is one tab stop and moves inside itself with the arrow keys; Home and End reach the ends. A panel anchored to a control closes on Escape and on a pointer outside it, and returns focus to the control. A modal dialog or drawer traps focus, makes the rest of the page inert and closes on Escape. Every menu answers the same keys: arrows, Home, End, Enter, Space and typeahead.
While a modal is open, the rest of the page is inert and does not scroll, as it is behind a native modal <dialog>. Toasts, tooltips, the panels the modal opens and every live region stay usable, and Tab from the dialog's last control reaches the toast stack and comes back. An overlay of your own that must stay usable while a modal is open, a chat launcher say, carries the data-sui-keep-live attribute. The marked element and everything inside it stay live, take focus, and join the Tab order after the dialog's last control, as the toast stack does, so keyboard and screen reader users reach it. A pointer reaches it only if it paints above the modal's scrim: a popover in the top layer does, while a dropdown appended to <body> with position: fixed stays live under the scrim. A mark on an element that contains the modal is ignored. Anything else outside the modal is inert until it closes.
Read the element's Keyboard table before you add a key handler of your own.
One color, from tokens, and nothing removes it. Buttons, checkboxes and custom controls draw outline: 2px solid var(--focus-ring) at outline-offset: 1px over box-shadow: var(--focus-shadow) on :focus-visible. A field (a text input, a picker's trigger, the listbox, the editor) draws a 2px edge in --focus-ring with a soft halo of the same color, and an invalid field keeps its danger edge and takes the ring outside it. The theme engine takes the focus color from the primary or the accent, never both: the one that stays clear of the danger color and needs the least change to reach 3:1 on the page, 4.5:1 in dark. A theme built in sui-theme-picker gets its own. To restyle focus, set the two tokens; do not set outline on an element.
Focus goes back where it came from. Dialog and drawer return it to the control that opened them, or to the nearest control where that control stood if it is gone; give either a returnFocus selector to send focus somewhere else on close. Confirm dialog and tour return it to the control that opened them; an anchored panel returns it to its trigger. When your own code removes the surface that holds focus, move focus yourself.
Text comes in three tiers. Each reads at least 4.5:1 on the grounds it sits on (a card, a well, the page and the raised panel of a menu or a popover), a visible step from the next. In the default light theme, on the page, the darkest of those grounds: --ink reads 14.07:1, --ink-muted is #47506D at 7.35:1 and --ink-faint is #656C80 at 4.83:1. So ink reads 1.91 times muted, and muted 1.52 times faint. In dark, muted reads at least 7:1 and faint at least 5.4:1. Faint carries card subtitles, field help text, character counters, timestamps and chart axis labels. A field's placeholder, --ink-placeholder, is #696F82 at 4.62:1. On the chrome, --ink-inverse-muted and --ink-inverse-faint tint the inverse ink at 0.82 and 0.68, which reads 11.15:1 and 8.02:1 on --surface-navy.
The faint ink falls under 4.5:1 on a hovered, selected or current row, so such a row paints its faint text in the muted ink. A selected, current or pressed row also swaps the muted ink for --ink-muted-on-wash, the muted ink fitted to read 4.5:1 on that wash. It is #414A69 in light and #C7CCD7 in dark, a step under the ink, so the row keeps two tiers. A hover is laid over a selection, so a selected row or option under the pointer still shows a step. In that passing state its secondary text takes the ink, which reads 5:1 or more there. When the keyboard moves the active option of a list whose focus stays on its field (a select, a combobox, a listbox), that option also takes the focus ring, inset, whether it is selected or not. A disabled row paints its secondary text in the placeholder ink and its badge in the neutral tone, so nothing in it reads louder than its label. On a disabled field's slab, secondary text takes the disabled ink, which reads 4.5:1 there.
The edge of a text field and of every other control is --control-border, #8A8D99, which reads 3.31:1 on the field's white and 3.05:1 on --canvas, the 3:1 that 1.4.11 asks of a control's boundary. A radio's ring is 2px wide, in the control edge mixed with a quarter of the ink, so the drawn circle reads 3:1 or more at any screen density. A checkbox's box takes the same mix, so the two marks read as one family. The chart axis, --chart-axis, reads 3.07:1 on the card. Every data series reads at least as much as the axis, in even steps: in light, 3.18, 5.22, 8.48 and 13.84:1 on the card.
Every shipped theme clears the same floors, and so does every theme the engine derives from a primary and an accent. There is one contrast level: a system set to ask for more contrast gets the same tiers.
Under forced-colors: active every element draws with the system palette: borders and outlines in CanvasText, a selected or focused item in Highlight and HighlightText, a disabled control in GrayText. Give any fill of your own a border, because forced colors drops backgrounds.
Every value-bearing control is a form-associated custom element. A plain <form> posts it under its name with no hidden input, an emptied field posts an empty string, and a required field that is empty sets valueMissing with a message, so form.checkValidity(), reportValidity(), aria-invalid and the invalid event all come from the browser with no library call.
Give every field a label. It renders as a <label for> pointing at the control; help and error are wired with aria-describedby, and the error line carries role="alert", so a message that appears after a submit is announced without stealing focus.
Under prefers-reduced-motion: reduce the token sheet sets --duration-fast, --duration-base, --duration-slow and --duration-overlay to 0ms, so every transition built on a duration token stops, an overlay's closing fade included. Build your own transitions on the same tokens and they stop too.
- Spinners slow, they do not stop. A stopped spinner reads as a hung page. Button, table and popup spinners go to a two second turn, the skeleton shimmer to three seconds, the progress sweep to 3.2 seconds.
- Scripted scrolling asks first.
sui-scroll-top,sui-anchorandsui-tourscroll withautoinstead ofsmooth;sui-tabssmooth-scrolls its bar only underprefers-reduced-motion: no-preference. - Nothing advances on its own.
sui-carouselandsui-galleriaread the preference every time they arm, wait while the pointer or focus is on the rail or the tab is hidden, and stop for good at the reader's first move. A change of the preference while the page is open is followed. - The scroll-reveal helper does nothing under the preference.
The elements name their own parts. None names your content.
- Name an icon-only control.
sui-icon-buttontakes its accessible name from its ownaria-label; there is noariaLabelprop. Without one the button is unnamed and gets no tooltip. The same goes forsui-icon-field,sui-speed-dial, and a select, multi-select, cascade select, tree select, time picker,sui-color-pickeror number field used with nolabel. - Name each copy when a page shows two.
sui-paginator,sui-breadcrumb,sui-side-nav,sui-bottom-nav,sui-dockandsui-filter-paneleach declare a landmark and read the host'saria-labelfor its name. Two unnamed copies are two landmarks a reader cannot tell apart. - Name a scroller you want announced.
sui-virtual-scrollermakes its viewport a tab stop either way, but it becomes a named region only when the host carries anaria-label. - Give the page its landmarks and a skip link. The elements render no
<main>, no<header>and no skip link. Put one<main>on the page, name each<nav>, and put a "Skip to content" link first in the body, pointing at<main>. - Announce what changes out of view.
sui-toast-hostandsui-messageare live regions: critical interrupts, the rest wait their turn. Anything else that changes where the reader is not looking needs anaria-liveregion of your own. - Pair a chart with a table. Every plot is one tab stop named by its heading and each visible series. The arrow keys walk its points and a reader hears each one's category, series and figure, and legend keys are real buttons with
aria-pressed. A table of the same figures still serves a reader who compares or copies them, so put one beside a plot that matters. - Underline links inside prose. The shipped sheet sets
a { color: var(--link); text-decoration: none; }, and--linkreads 1.6:1 against--ink-muted, so inside a paragraph the tint alone marks the link, which fails WCAG 1.4.1. Underline prose links in your own stylesheet. - Audit the DOM as drawn. axe-core run inside the page sees only the children the page gave each element, not what the element draws. It reports a page header's heading as missing and checks nothing inside the element. Screen readers, the browser's accessibility tree and checkers that run as a browser extension read the DOM as drawn. Call
auditTree(true)on a settled page before the run andauditTree(false)after; Cypress runs the test in another frame, so pass it the page's document fromcy.document()as the second argument. - Let a fill carry its own ink.
--brand-soft-ink,--accent-soft-inkand--danger-soft-inkexist for the fills they name. A theme ink painted onto a status fill gives a 2.6:1 pill.
Three checks run on every commit. axe-core reads every page of this site in the light and dark themes and fails the build on a violation outside the few held exceptions, none of which is a text tier. A contrast audit fails the build on any painted token pair under its floor in every shipped theme and in three hundred derived ones. The browser tests drive each element's keyboard table with real key presses in Chromium, Firefox and WebKit. The forced colors and right-to-left sweeps run before a release.
No screen reader session has been run. Roles, names, values and states are read from Chrome's accessibility tree, and behavior under NVDA, JAWS and VoiceOver is inferred from that tree.
sui-multi-selectreads a word twice in its combobox value, because each chip's remove control adds its own label to the value string.