Skip to content

Accessibility

Built to help your app meet WCAG 2.2 AA

Skribly UI aims at WCAG 2.2 Level AA in everything it ships. This page says how, what the tests check, and what stays with the app that uses it.

The target, and what it is notLink to the section The target, and what it is not

Information: No claim of conformance

Conformance belongs to whole pages, not to parts. A library cannot make an app conformant on its own, and Skribly UI has not been audited or certified. It removes the common failures from the parts it supplies, and the app still owns its content, its words, its page structure and its own CSS.

Every component is designed against the AA criteria of WCAG 2.2, built on React Aria so keyboard, focus and screen reader behaviour follow the ARIA Authoring Practices, and documented with its own accessibility notes on its page.

What the tests checkLink to the section What the tests check

  • axe on every demo

    207 demos run through axe-core in the light, dark and high contrast themes: 621 runs on every npm test.

  • Contrast of the token pairs

    Text and tint pairs, control borders and the focus ring are measured in each theme against 4.5:1 and 3:1, or 7:1 and 4.5:1 in high contrast.

  • Every component is shown

    A test fails when an exported component has no demo, so nothing ships without being run through axe.

  • Behaviour per component

    Each component has Testing Library tests for its keyboard paths, accessible names, states and live messages.

  • This site in a real browser

    Before each release every page of this site is opened in Chromium at 320 and 1280 pixels wide and run through axe with layout, so target size, colour contrast in all three themes, links in text and scroll boxes are measured too. A page that scrolls sideways or is not found fails the build.

The unit tests run in jsdom, which has no layout. axe’s own contrast rule is off there, which is why the token pairs are checked from their values, and why the built site is checked again in a browser. Some things no automated test can see:

  • what a real screen reader says, and in which order;
  • whether the focus ring is visible against what is behind it;
  • layout at 400% zoom and on a 320px screen;
  • the words and the content your app puts in the components.

Those are checked by hand during review, and your app should check them too.

WCAG 2.2 AA criteriaLink to the section WCAG 2.2 AA criteria

The criteria where the library does real work, how its components meet them, and the part that stays with your app.

WCAG 2.2 success criteria, level, what the library does and what the app owns
CriterionLevelHow the components meet itWhat your app owns
1.1.1Non-text contentAIcons are hidden from screen readers or named. IconButton needs a label; Logo is decoration unless given one.Alt text for the images it shows.
1.3.1Info and relationshipsANative elements and React Aria roles. Labels, help and errors are linked to their control; tables have header cells; heading levels are props.The heading outline and landmarks of each page.
1.3.5Identify input purposeAAFields pass autoComplete through; the sign-in and reset forms set it.autoComplete on fields for personal data.
1.4.1Use of colourAEvery tone pairs its colour with a word, an icon shape or both.The same rule in its own views and charts.
1.4.3Contrast (minimum)AAText pairs reach 4.5:1 in light and dark and 7:1 in high contrast. The tests check them.Its own colours, and text over images.
1.4.4Resize textAASizes are in rem and follow the browser text size.No fixed pixel font sizes in its own CSS.
1.4.10ReflowAAComponents wrap or stack down to 320px; wide tables and code scroll in their own focusable box.A page layout that reflows at 320px.
1.4.11Non-text contrastAAControl borders and the focus ring reach 3:1 in light and dark and 4.5:1 in high contrast, checked by the tests.Its own icons and chart marks.
1.4.12Text spacingAAText wraps and boxes grow with it rather than clipping.Fixed-height boxes of its own.
1.4.13Content on hover or focusAATooltips show on focus, stay while hovered and close with Escape.Nothing extra.
2.1.1KeyboardAEvery control works from the keyboard. Dragging always has a keyboard path.Custom widgets it builds itself.
2.1.2No keyboard trapADialogs keep focus on purpose, close with Escape and return focus to the trigger.Nothing extra.
2.2.1Timing adjustableASuccess and information toasts stay at least 5 seconds and pause on hover or focus; warnings, errors and toasts with an action stay until closed.No timeouts on its own messages.
2.3.1Three flashesANothing flashes. Spinners turn; pulses fade slowly and stop under reduced motion.Its own animations and video.
2.4.1Bypass blocksASkipLink and MainContent give a jump to the content; layouts use landmarks.A skip link on every page.
2.4.3Focus orderAReading order matches the visual order; focus returns after overlays close.The order of its own layout.
2.4.6Headings and labelsAAEvery label and heading is a prop, so it can describe its purpose.Words that describe the purpose.
2.4.7Focus visibleAAOne focus ring for every element from the page defaults, 3px with the strong focus setting.Not removing outlines in its own CSS.
2.4.11Focus not obscured (minimum)AARings are drawn inside boxes that clip; overlays open beside their trigger.scroll-padding under its sticky headers and footers.
2.5.3Label in nameAThe visible label is the accessible name; icon buttons show their name as a tip.Keep visible words and labels the same.
2.5.7Dragging movementsAAReordering has NudgeButtons, splitters move with keys, file drops have a button.A single-pointer path for its own drag features.
2.5.8Target size (minimum)AATargets are at least 24 by 24 pixels, also in the compact density.Spacing between its own small targets.
3.2.2On inputAChoosing an option never submits a form or moves to another page by itself.The same in its own handlers.
3.3.1Error identificationAErrors are text with an icon, set aria-invalid and are linked to their field.Error messages that say what is wrong.
3.3.2Labels or instructionsAFields have a visible label, a required or optional mark and help text.The words of each label and hint.
3.3.3Error suggestionAAerrorMessage holds the fix, and FormErrorSummary links to each field.Messages that say how to fix it.
3.3.8Accessible authentication (minimum)AAThe sign-in forms allow paste and password managers through autocomplete.No puzzles or copying codes by hand.
4.1.2Name, role, valueAReact Aria sets roles, states and properties; the types require every name.Names for its own custom controls.
4.1.3Status messagesAAToasts, flash messages, live alerts, form statuses and announce() use polite or assertive live regions.Announcing results of its own async work.

KeyboardLink to the section Keyboard

The keys follow the ARIA Authoring Practices, so they are the ones screen reader and keyboard users already know. Each component page lists its own keys.

Everywhere

KeyWhat it does
TabMoves to the next control. The first press on a page shows the skip link.
Shift+TabMoves to the previous control.
F6Moves focus into the toast stack while toasts are shown.
EscapeCloses the open menu, popover, tooltip or dialog and returns focus to what opened it.

Buttons, toggles and links

KeyWhat it does
EnterorSpacePresses a button or flips a toggle, a checkbox or a switch.
EnterFollows a link. Links also open the browser menu with a right click.
←or→Moves between the buttons of a toolbar, which is a single Tab stop.

Tabs, segmented controls and radio groups

KeyWhat it does
←or→Moves and selects; Up and Down in vertical tab lists.
HomeorEndGoes to the first or the last tab.
DeleteorBackspaceCloses the focused tab of ClosableTabs.

Menus, selects and search lists

KeyWhat it does
EnterorSpaceor↓Opens a menu or a select on its first item; Up opens a menu on its last.
↑or↓Moves through items while focus stays in the search field of GlobalSearch, CommandPalette and QuickOpen.
A–ZTyping a letter jumps to the first matching item.
EnterRuns or picks the highlighted item.

Lists, tables and trees

KeyWhat it does
↑or↓Moves between rows; arrows move between cells in a DataTable.
HomeorEndorPage UporPage DownJumps through long lists, also through rows a VirtualList has not drawn yet.
SpaceSelects a row.
→or←Opens or closes a tree branch, or moves to its child or parent.
EnterPicks a dragged item up and drops it; arrows choose the place and Escape cancels.

Sliders, splitters and date fields

KeyWhat it does
←or→or↑or↓Steps a slider, moves a splitter or changes a date segment.
Page UporPage DownJumps further, or by a month in the calendar.
HomeorEndGoes to the limits.
EnterCollapses and restores a split view or a dock.

Text entry

KeyWhat it does
EnterSends from the Composer, adds a chip in ChipInput, submits a search field.
Shift+EnterAdds a line in the Composer.
Ctrl+Enteror⌘+EnterPosts from a CommentForm.
EscapeClears a search field.

Screen readers and live messagesLink to the section Screen readers and live messages

Every component has a name, a role and a state. Names come from props, never from the library, so they are in the app’s language. Status words such as “Error” are read before the message, so the tone never rests on colour and icon.

Messages that appear later are read through live regions: polite for success and information, which waits for the screen reader to finish, and assertive for errors that stop the reader. LiveRegion and announce() do the same for an app’s own messages. The messages pattern says which component to use.

Reduced motionLink to the section Reduced motion

Motion is short and never needed to understand a change. With prefers-reduced-motion: reduce, or the a11y-reduce-motion class from the reader settings, the duration tokens drop to almost zero, pulses stop, theme changes switch at once and scrolling jumps. Spinners keep turning slowly, because a still spinner would say the work has stopped.

Forced coloursLink to the section Forced colours

Windows high contrast replaces every colour with a few system colours. Components keep their shape there: badges and tiles have transparent borders that become visible, the current item gets an underline or a Highlight bar, selected options use Highlight, and skeletons get an outline. Colour swatches, such as on the token tables, keep their real colours.

The high contrast theme is a separate thing: a theme of the library for readers who want strong contrast with the Skribly look.

Zoom, reflow and text spacingLink to the section Zoom, reflow and text spacing

  • Text sizes are in rem, so the browser text size setting scales every component.
  • At 400% zoom, a 1280px window is 320px wide. Components wrap, stack and truncate with the full text kept for screen readers, and nothing scrolls sideways except data tables and code, which scroll inside their own box.
  • Fields use 16px text below 640px, so phones do not zoom in when a field takes focus.
  • Text containers grow with extra line, word and letter spacing instead of clipping it.

Target sizeLink to the section Target size

Pointer targets are at least 24 by 24 pixels, the AA minimum of WCAG 2.2. Controls are 36px tall by default; the compact density makes them 30px, still above the minimum. Small close buttons get a 24px hit area even where the icon is smaller, and the steppers of the reader settings are 44px.

How to test your appLink to the section How to test your app

  1. Keyboard onlyPut the mouse away. Reach every control with Tab, use it, and see the focus ring all the way.
  2. A screen readerVoiceOver on macOS and iOS, NVDA on Windows, TalkBack on Android. Listen to names, states and messages.
  3. Zoom and 320pxZoom to 200% and 400%, and use a 320px wide window. Nothing should be cut off or scroll sideways.
  4. Forced colours and motionEmulate forced-colors and prefers-reduced-motion in the browser developer tools.
  5. Automated checksRun axe in unit tests and in end-to-end tests of real pages, where it can measure contrast.
  6. Your own contentRead every label, error and empty state aloud. Does it say what happened and what to do?
An axe check in Vitesttsx
import axe from 'axe-core'
import { render } from '@testing-library/react'

it('has no axe violations', async () => {
  const { container } = render(<SettingsPage />)
  const results = await axe.run(container, { rules: { 'color-contrast': { enabled: false } } })
  expect(results.violations).toEqual([])
})
An axe check in Playwright, with contrastts
import AxeBuilder from '@axe-core/playwright'

test('settings page', async ({ page }) => {
  await page.goto('/settings')
  const results = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa', 'wcag21aa', 'wcag22aa']).analyze()
  expect(results.violations).toEqual([])
})

Search this site

Arrows move through the results, Enter opens one, Esc closes. Ctrl K /