Accessibility
ARC UI targets WCAG 2.1 Level AA. Components ship with semantic HTML, keyboard support, and ARIA attributes built in. This site is the test bed.
Automated Audit
Claims are cheap, so we audit. Every component documentation page, live demos included, is checked with axe-coreagainst the fully rendered DOM, shadow roots included, in both themes. CI fails on any serious or critical violation, color contrast included. No exclusions.
ARIA Attributes
Roles are set automatically: arc-dialog is a role="dialog"managing aria-modal; arc-tabs wires uptablist/tab/tabpanel. Anything extra passes through from the host element:
<!-- Labels -->
<arc-button aria-label="Close dialog">
<arc-icon name="x"></arc-icon>
</arc-button>
<!-- Descriptions -->
<arc-input
label="Password"
aria-describedby="pw-hint"
></arc-input>
<span id="pw-hint">Must be at least 8 characters</span>
<!-- Live regions -->
<arc-alert aria-live="polite" variant="success">
Changes saved successfully
</arc-alert>
Keyboard Navigation
Every interactive component is fully keyboard-driven. Don't take our word for it. This one is live. Tab to it and drive it from the keyboard:
| Component | Keys | Action |
|---|---|---|
| Button | Enter / Space | Activate |
| Modal | Escape | Close |
| Tabs | Arrow Left/Right, Home / End | Switch tab |
| Accordion | Enter / Space | Toggle panel |
| Select | Arrow Up/Down | Navigate options |
| Select | Enter | Select option |
| Select | Escape | Close dropdown |
| Toggle | Space | Toggle on/off |
| Checkbox | Space | Toggle checked |
| Drawer | Escape | Close |
| Tooltip | Escape | Dismiss |
Focus Management
ARC UI ships two focus indicator styles as design tokens. Tab through these to compare them:
Focus shows only on keyboard navigation (:focus-visible), never on mouse clicks. Click the cards above and nothing appears. Overlays like arc-dialogand arc-drawer trap focus while open.
/* Customize focus styles globally */
:root {
--focus-ring: 0 0 0 2px var(--bg-deep),
0 0 0 4px var(--accent-primary);
--focus-glow: var(--focus-ring),
0 0 16px rgba(var(--accent-primary-rgb), 0.3);
}
Screen Readers
Shadow DOM can complicate screen reader access; ARC UI's patterns avoid the traps:
- Slotted content stays in the light DOM, where screen readers reach it directly.
- ARIA attributes set on the host element are reflected into the shadow root where needed.
- Form elements take a
labelattribute, so they are always announced.
<!-- Use slot for screen-reader-accessible content -->
<arc-card>
<h3>Account Settings</h3>
<p>Manage your profile and preferences.</p>
</arc-card>
<!-- Use label attribute for form elements -->
<arc-input label="Email address"></arc-input>
<arc-select label="Country">
<option value="us">United States</option>
<option value="uk">United Kingdom</option>
</arc-select>
Color & Contrast
The audit above measures actual rendered ratios in both themes. These are results, not targets:
--text-primaryon--bg-deepexceeds 15:1.--text-mutedand--text-ghostmeet the 4.5:1 ratio for normal text in both themes.- The feedback colors (
--color-success,--color-warning,--color-error,--color-info) pass in both dark and light modes.
Reduced Motion
With prefers-reduced-motion set, every transition and animation collapses. Functionality is unchanged; only the motion goes:
/* Built into ARC UI components */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
The same rule reaches your own components if they adopt tokenStyles, and it has one consequence worth designing for. A collapsed animation runs once, at once, and then the element shows its un-animated styles. Whatever the base styles say is what a reduced-motion user sees. So put the resting state there, and let the keyframes move away from it. A two-frame sprite that swaps layers by opacity shows both frames stacked if both are visible by default. Hide the second frame in the base styles, and the keyframes that reveal it never get time to:
.frame-b { opacity: 0; } /* resting state: frame A only */
.frame-a { animation: swap-a 800ms steps(1) infinite; }
.frame-b { animation: swap-b 800ms steps(1) infinite; }
@keyframes swap-a { 50% { opacity: 0; } }
@keyframes swap-b { 50% { opacity: 1; } }
Without JavaScript
Web Components need JavaScript to upgrade. Until that happens (a failed script, a slow network, JS disabled), a custom element is an unknown tag rendering only its light DOM. What you put inside the element is what everyone sees first.
This matters most for links. An href on the custom element itselfcannot produce a working link without JavaScript: the attribute sits on the element, and the real <a> only exists in a shadow root that is never created. Slot a real anchor instead and the link is ordinary HTML from the first byte:
<!-- Works without JS, and before the element upgrades -->
<arc-button variant="primary">
<a href="/get-started">Get started</a>
</arc-button>
<arc-navigation-menu>
<arc-nav-item><a href="/docs">Docs</a></arc-nav-item>
<arc-nav-item><a href="/pricing">Pricing</a></arc-nav-item>
</arc-navigation-menu>
<!-- Needs JS to become a link at all -->
<arc-button variant="primary" href="/get-started">Get started</arc-button>
On upgrade the component adopts that anchor as its control rather than rendering a second one, so there is only ever one link in the accessibility tree. Thehref form stays fully supported. Prefer it inside app shells where JavaScript is guaranteed, and the slotted form on anything public, indexed, or first-load critical. This applies to arc-button,arc-icon-button, arc-link, arc-card,arc-nav-item, arc-breadcrumb-item, andarc-sidebar-link.
The stylesheet hides components only until they upgrade, and that hide is gated on scripting being available, so with JS disabled your content stays visible rather than being hidden forever waiting for an upgrade that never comes:
/* Shipped in base.css */
@media (scripting: enabled) {
:is(arc-button, arc-link /* … */):not(:defined) { opacity: 0; }
}
Components whose content comes from properties rather than slots, such asarc-feature-card (which renders from heading anddescription), have no light DOM to fall back to and stay blank without JavaScript. Author their content as slotted markup, treat them as enhancement-only, or server-render them.
See Also
- Server Rendering: content in the payload before JS runs
- Getting Started: installation and setup