Skip to content
Español

Navigate

Type to search. Press Escape to close.

    Foundations

    Accessibility

    Accessibility is split between the framework and the page. iVOLT can give you visible focus, contrasting colours, keyboard behaviour and correct ARIA state on its own components. It cannot give you accessible names, a sensible heading order or meaningful labels — those are written by whoever builds the page. This page draws the line and says what has actually been tested.

    Tested with: axe on the fixtures, in both themes, in Chromium, Firefox and WebKit. Not verified: screen reader sessions and physical devices.

    The claim we make, and the only one: the evaluated fixtures show no serious or critical axe findings in both themes, and they pass a manual keyboard review. That is not a certification of anything.

    What the framework does

    Visible focus

    Focus is a token, not a per-component decision. .iv-root :focus-visible draws var(--iv-focus-width) (2px) of solid var(--iv-color-focus) with var(--iv-focus-offset) (2px) of offset, and components that need a different placement re-declare the same tokens rather than inventing a ring — the dropdown item, for instance, uses a negative offset so the outline stays inside the scrolling menu instead of being clipped.

    The focus colour is #1D4FC4 in light and #73DFFF in dark, both at or above 3:1 against either surface. Nothing in the package sets outline: none without replacing it. Using :focus-visible rather than :focus means a mouse click on a button does not leave a ring, while the same button focused with Tab does.

    Every state of a buttonfixtures/button/states.htmlOpen alone — Every state of a button
    <div class="iv-cluster">
      <button class="iv-button iv-button--primary" type="button" disabled>Disabled</button>
      <a class="iv-button iv-button--secondary" href="#docs" aria-disabled="true">Disabled link</a>
      <button class="iv-button iv-button--primary" type="button" aria-busy="true">Saving…</button>
    </div>

    Colour and contrast

    The semantic tokens were chosen against measured ratios, not by eye. Values below are the intended contrast for the light and dark themes, measured over bg unless noted.

    TokenLightDarkContrast (light / dark)
    text#0B1230#EEF2FB18.4 / 18.0
    text-muted#4F5B7A#A8B3CC6.8 / 9.6 (8.7 over surface-raised)
    border-strong (controls)#6B7896#6B78964.4 / 4.6 (≥ 3:1)
    border (decorative)#D3DAEA#1C2544not required
    primary / on-primary#1D4FC4 / #FFFFFF#66B1FF / #06122B7.1 / 8.9
    accent / on-accent#0B6B8A / #FFFFFF#73DFFF / #06122B5.5 / 13.2
    success / on-success#4F7A12 / #FFFFFF#8BBF3A / #06122B5.1 / 9.2
    danger / on-danger#D63A31 / #FFFFFF#FF9AA4 / #06122B4.7 / 10.0
    warning / on-warning#8F5C00 / #FFFFFF#FBBF24 / #06122B5.0 / 12.1
    info / on-info#3D47C2 / #FFFFFF#8B95FF / #06122B7.3 / 7.5
    focus#1D4FC4#73DFFF≥ 3:1 on both surfaces

    The worst real case is surface-raised, where border-strong measures 4.13 in dark and 4.12 over surface in light; both stay well above 3:1. Each *-subtle background is paired with its own foreground and was measured against it, so an alert or badge is never a coloured surface with unreadable text on top.

    One consequence to know about: primary and focus share the same cobalt, and in dark focus matches accent instead. That is intentional — focus and the primary are distinguished by shape (a subtle-background badge or alert, an offset focus ring), not by hue. If your interface needs colour to separate them, redefine --iv-color-success. More generally, never let colour be the only thing carrying a meaning: an error state needs text, an icon or both.

    These numbers describe the tokens. Override a token with your own brand colour and they no longer apply — re-measure.

    Target size

    Interactive targets aim for at least 24×24 CSS pixels. The icon-only button is laid out as a square that does not go below that size at any of its sizes, and compact inline targets such as breadcrumb links carry a min-block-size of 1.5rem. If you build your own control, keep the padding that gets it there; shrinking a button with your own CSS can take it under the threshold.

    Keyboard and ARIA on the JavaScript components

    Six families need JavaScript: disclosure, tabs, dialog, drawer, dropdown and toast. init(document) — called explicitly, or on load through the auto entry — finds elements marked with data-iv-component and takes over their keyboard and ARIA state. Roles and states it needs are added when missing, so the markup you write stays small, and destroy() restores the attributes exactly as it found them.

    The shared keyboard vocabulary is the platform one: Tab to move, Enter and Space to activate, Escape to dismiss what is dismissible, arrow keys with Home and End to move inside a composite widget such as tabs or a menu. Each component page documents its own keys; that is the authoritative list.

    Two structural choices help here. Dialog and drawer are real <dialog> elements, so modal focus containment, Escape and the top layer come from the browser rather than from a hand-written focus trap. The dropdown is built on <details>, so it opens and closes with the keyboard even before the script runs. The toast region is given role="region" when it lacks one, because a labelled element with no role announces nothing.

    Motion and contrast preferences

    @media (prefers-reduced-motion: reduce) sets --iv-motion-fast, --iv-motion-base and --iv-motion-slow to 0ms, and the components that animate — button, dialog, dropdown, progress, skeleton — additionally drop their transforms and looping animations in the same query. States remain fully visible: reduced motion removes the movement, not the feedback.

    Under prefers-contrast: more the token stylesheet raises decorative borders (--iv-color-border) to the control-strength value (--iv-color-border-strong), so card and table edges reach 3:1. Nothing else changes; forced-colors mode is untested at this stage.

    What the author has to do

    These are the failures that show up in real projects built on any framework, iVOLT included.

    • Accessible names. An icon-only button is an unlabelled button until you give it aria-label or visually hidden text with iv-u-sr-only. The same applies to a close button, a menu trigger and an icon link.
    • Heading order. One <h1> per page and no skipped levels. Heading size in iVOLT is a token, so use the level the document needs and change its size with a utility if it looks too large.
    • Labels, not placeholders. Every field needs a real <label for>. A placeholder disappears as soon as someone types.
    • Help and error text. Connect it with aria-describedby so it is announced with the field, and mark the field aria-invalid="true" when it is in error. Error text must say what to do, not only that something is wrong.
    • A no-JavaScript fallback. The CSS-only families work without a script at all. For the interactive ones, write markup that degrades: <details> for disclosure and dropdown, a :target fallback for dialog and drawer, tab panels that are all visible before the script upgrades them. If the script fails to load, the content must still be reachable.
    • Language and landmarks. lang on <html>, a <main>, a skip link, and aria-label on repeated landmarks such as two navigations.
    • Content contrast. Text you place over an image, a gradient or a brand colour of your own is outside the token table.
    A field in errorfixtures/form/error.htmlOpen alone — A field in error
    <div class="iv-stack" style="max-width: 28rem">
      <div class="iv-field">
        <label class="iv-label" for="f-email">Work email</label>
        <input class="iv-input" id="f-email" type="email" value="dave@" aria-invalid="true" aria-describedby="f-email-error" autocomplete="email">
        <p class="iv-field__error iv-u-m-0" id="f-email-error">Enter a complete email address, like [email protected].</p>
      </div>
      <div class="iv-field">
        <label class="iv-label" for="f-locked">Plan</label>
        <input class="iv-input" id="f-locked" type="text" value="Team (locked)" disabled>
      </div>
    </div>

    How it is tested

    CheckScope
    axe-core through Playwright24 fixtures × light and dark themes, plus the dialog in its open state; tags wcag2a wcag2aa wcag21aa wcag22aa; the gate fails on any serious or critical finding
    Manual keyboard reviewEvery interactive family: reach, activate, dismiss, and where focus lands afterwards
    Three enginesThe browser suite runs in Chromium, Firefox and WebKit (IVOLT_ALL_BROWSERS=1)
    Overflow and RTL320–1920px with no horizontal overflow, and right-to-left rendering, because logical properties are what make that work
    Fixtures as the single sourceThe same fixture file is the docs preview, the copyable snippet and the test subject, so what is tested is what is documented

    Automated tooling catches a minority of issues. axe cannot tell you that a label is wrong, that a heading lies about the structure, or that a focus order is confusing; the manual review exists for that, and it is a review, not a proof.

    What we do not claim

    • Not “100% accessible”, not “WCAG certified”, not “conformant” as a property of the framework. Conformance is a property of a finished page, and most of that page is yours.
    • Screen reader testing has not been run. NVDA and VoiceOver passes are on the backlog as a P1 item and are recorded as not verified. Until then, announcements are inferred from the ARIA we set, not observed.
    • No Lighthouse run yet; that is scheduled for a later phase.
    • Test coverage is Chromium, Firefox and WebKit through Playwright, which is not the same as testing physical devices, assistive technologies or older browsers.
    • Findings in your own pages are not covered by any of the above. Run axe on the page you ship.

    If you find an accessibility bug in a component or a fixture, it is a bug — open an issue with the fixture name, the browser and the steps.