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
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>orFieldLabel; icon-only buttons needaria-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-invalidwith aFieldErrorthat says what went wrong and how to fix it. - Keep the heading outline. One
h1per page, and no skipped levels. - Give images alternatives. Meaningful images need
alt; decorative ones needalt="". - 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:
- 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. - Keyboard — walk every flow with the keyboard only: focus order, visible focus, no traps, Esc closes overlays.
- Screen readers — check key flows with VoiceOver (macOS, iOS) and NVDA or JAWS (Windows).
- Zoom and preferences — 200% and 400% zoom, reduced motion, increased contrast and forced colors.