Skip to content
TopClass UI

Accessibility

WCAG 2.2 AA, checked in a real browser: axe, target size, the focus ring and contrast.

Contrast

Contrast is measured from the real token values in every mode, as part of the build. Anything within 0.15 of a threshold counts as failing.

What, Minimum
WhatMinimum
Body and muted text4.5:1
A tone text on its own soft tint4.5:1
A control boundary, on every surface it can sit on3:1
The focus ring3:1
Icons that carry meaning3:1

Target size

Every interactive element is at least 24 by 24 CSS pixels, and grows to 44 on touch and other coarse pointers.

save-button.tsx
import { TOUCH_TARGET } from 'topclassui/classes'

export function SaveButton() {
  return <button type="button" className={TOUCH_TARGET}>Save</button>
}

The shared class every control carries, and your own controls can carry it too. Its value: min-h-[var(--tc-target-min,24px)] min-w-[var(--tc-target-min,24px)] pointer-coarse:min-h-[var(--tc-target-coarse,44px)] pointer-coarse:min-w-[var(--tc-target-coarse,44px)]

Every drag has a keyboard path

WCAG 2.5.7. A Slider takes arrow keys. A Drawer and a Toast always render a close button.

Focus is always visible

One focus ring on every control: a CSS outline with an offset, so the gap shows the real background on any surface. --tc-ring reaches 3:1 on every surface.

home-link.tsx
import { FOCUS_RING } from 'topclassui/classes'

export function HomeLink() {
  return <a href="/en" className={FOCUS_RING}>Home</a>
}

Its value: outline-none focus-visible:outline-solid focus-visible:outline-(length:--tc-ring-width) focus-visible:outline-offset-(length:--tc-ring-offset) focus-visible:outline-ring

Labels are enforced by the types

An Input outside a Field needs an id, an aria-label or an aria-labelledby, and TypeScript rejects one without.

Wrong
<Input placeholder="Email" />
// Type error: id, aria-label or aria-labelledby is required
Right
import { Field } from 'topclassui/field'
import { Input } from 'topclassui/input'

export function Email() {
  return (
    <Field name="email" label="Email">
      {(control) => <Input {...control} type="email" />}
    </Field>
  )
}

Automatic and manual

Check, How, When
CheckHowWhen
Contrastcheck-contrast.tsEvery pnpm check
ARIA and rolesaxe-core in every demoEvery pnpm check
Target sizeA browser test measures every focusable elementEvery pnpm check
Keyboard behaviourA test per interactive component, following the ARIA Authoring PracticesEvery pnpm check
Screen readerNVDA and VoiceOver, by handBefore each release
Forced colorsWindows high contrast, by handBefore each release

Known gaps

Contrast is measured on every build. Axe, target size, the focus ring and long text inside its control are checked in a real browser on every demo, in 3 modes and 2 directions, and the interactive components have keyboard tests. Screen readers (NVDA, VoiceOver) and forced colors are checked by hand, and that has not been done yet.

Forced-colors mode is a manual check. A layout that relies on background fills disappears there, so every component is drawn with borders.
NextComponents