Skip to content

Accessibility

prfct targets WCAG 2.2 AA by construction, in its palette, its primitives and its defaults, and automated tests verify every page.

Accessibility in prfct isn't a checklist at the end. It's built into the layers everything else sits on: a palette whose contrast is solved mathematically, primitives from Base UI that implement WAI-ARIA patterns, and component defaults that make the accessible choice the easy one.

What you get for free

AreaHow prfct handles it
ContrastText steps are solved for ≥ 4.5:1 on their surfaces; control boundaries meet 3:1 (WCAG 1.4.11). See Color.
KeyboardEvery interactive component is operable with the keyboard, with roving focus, typeahead and Esc where the ARIA pattern expects it.
FocusA visible 2px focus indicator, drawn with outline so it survives forced-colors mode. Its color is solved for 3:1 against every surface, for any brand color: see Theming.
SemanticsBase UI renders native elements and correct roles, states and relationships (aria-expanded, aria-controls, aria-describedby…).
OverlaysDialogs trap focus and return it to the trigger, hide the rest of the page from assistive technology, and take their accessible name from DialogTitle.
Target sizeButton, Input, Select and Toggle are at least 28px tall at every size; only the link button variant, which sits in running text, is shorter. Checkboxes, radios, switches and slider thumbs are smaller, and extend their hit area invisibly to 24×24px or more (WCAG 2.2, 2.5.8).
Motionprefers-reduced-motion collapses all transitions; nothing depends on animation.
Contrast preferencesprefers-contrast: more strengthens borders and secondary text.
Forced colorsIn Windows High Contrast the system palette replaces the theme and removes fills and shadows. Borders and outlines carry the structure, floating surfaces get an edge, and pressed, current, highlighted and selected states take the system's selection colors.
Zoom & text sizeType is set in rem, so it follows the browser's font size. Layouts reflow without horizontal scrolling down to a 320px viewport, which is 400% zoom on a 1280px window (WCAG 1.4.10).
LanguageInter covers Latin Extended-A, so Polish, Czech, Turkish and other languages render with real glyphs.

Focus

Press Tab to move through the controls below.

import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Switch } from "@/components/ui/switch"

export default function FocusRing() {
  return (
    <div className="flex w-full max-w-sm flex-col gap-4">
      <p className="text-sm text-muted-foreground">
        Press{" "}
        <kbd className="rounded border bg-surface px-1 font-mono text-xs">
          Tab
        </kbd>{" "}
        to move through the controls below.
      </p>
      <Input placeholder="Text fields show a halo" aria-label="Example input" />
      <div className="flex items-center gap-2.5">
        <Checkbox id="focus-demo-check" />
        <Label htmlFor="focus-demo-check">Discrete controls show a ring</Label>
      </div>
      <div className="flex items-center gap-2.5">
        <Switch id="focus-demo-switch" />
        <Label htmlFor="focus-demo-switch">Offset from the edge</Label>
      </div>
      <div className="flex gap-2">
        <Button variant="outline">Cancel</Button>
        <Button>Continue</Button>
      </div>
    </div>
  )
}

prfct uses two focus styles:

  • Discrete controls — buttons, checkboxes, switches, tabs — show a 2px ring offset 2px from the edge.
  • Text fields show a brand border and a 3px halo, because a ring outside a bordered field reads as a second border.

Both follow :focus-visible: controls show their ring only when focus comes from the keyboard, so mouse users don't see rings on click. Text fields show their halo whenever they're focused, however focus arrived — a focused field is where your typing goes, so it should always say so.

Your responsibilities

Components can't know everything about your content. These are yours:

  • Label everything. Every input needs a visible <Label> or FieldLabel; icon-only buttons need aria-label.
  • Write meaningful text. Link and button labels must make sense out of context: Download invoice, not Click here.
  • Describe errors in words. Pair aria-invalid with a FieldError that says what went wrong and how to fix it.
  • Keep the heading outline. One h1 per page, and no skipped levels.
  • Give images alternatives. Meaningful images need alt; decorative ones need alt="".
  • Don't use color alone. Status needs an icon or text, too.
  • Announce async results. Use a toast or a live region for results that appear without a page change.

Testing

Test your product the way prfct's own site is tested:

  1. Automated — scan every page with axe-core in both modes. prfct's test suite does this for every page of this site, in light and dark mode (pnpm test:a11y), and tabs through its most interactive pages checking that every stop shows a focus indicator.
  2. Keyboard — walk every flow with the keyboard only: focus order, visible focus, no traps, Esc closes overlays.
  3. Screen readers — check key flows with VoiceOver (macOS, iOS) and NVDA or JAWS (Windows).
  4. Zoom and preferences — 200% and 400% zoom, reduced motion, increased contrast and forced colors.
Automated tools catch a fraction of issues
axe and Lighthouse are a floor, not a finish line. Missing labels and low contrast are easy to detect; confusing focus order, unclear errors and unannounced updates are not. Always test with a keyboard and a screen reader.