Skip to content

ARIA tabs · keyboard focus · reduced motion

Accessibility

Interaction patterns — focus, keyboard support, motion, labelling — are the component's job, and every component here handles its own. Colour, contrast and type size are the theme's job: components read --theme-* tokens and never hardcode a colour, so an accessible result depends on the values a theme gives those tokens.

What each component handles

Four things every component here gets right without a theme having to ask for them.

  • ARIA and semantics icon

    ARIA & semantics

    Interactive widgets carry the roles and states screen readers expect — tabs are a real tablist, empty media placeholders announce as an image with a label rather than nothing at all.

  • Keyboard access icon

    Keyboard access

    Every control a mouse can reach, a keyboard can too — including the horizontal scroll on a wide table, which is easy to strand behind a mouse-only drag.

  • Reduced motion icon

    Reduced motion

    Anywhere a component animates on its own, it checks prefers-reduced-motion first and shows the resting state instead of skipping straight to motion.

  • Images and alt text icon

    Images & alt text

    Image props resolve alt text from the media item itself, falling back sensibly when only one image variant has it set. An empty alt is left empty on purpose for decorative images, not forgotten.

Per-component notes

Where the pattern is more specific than "handled," here's the mechanism.

  • Every tab is a real tab, not just a styled button

    Screen readers get a genuine tablist/tab/tabpanel structure — aria-controls, aria-selected, aria-labelledby, and a roving tabindex so only the active tab sits in the normal tab order. Arrow keys and Home/End move between tabs, straight from Bootstrap's own Tab component.

  • A missing image never reads as nothing

    Hero Banner, Ping Pong, Statistic, CTA Icon Item, Media, and Icon Media all announce their empty-state placeholder as role="img" with a label describing what's missing, instead of a blank, unlabelled div. Decorative glyphs — eyebrow icons, CTA arrows — stay aria-hidden so they don't clutter the announcement.

  • Reduced motion means an instant number, not a skipped animation

    Statistic checks prefers-reduced-motion before it counts up — the final number renders immediately instead of animating toward it, so the information arrives exactly as fast as everything else on the page.

  • A wide table never traps a keyboard user

    Table's horizontally-scrolling wrapper is a focusable role="region" with an aria-label, so scrolling a wide table sideways takes a keyboard, not a mouse.

  • Play a video without losing your place

    Activating Video facade's poster moves focus straight into the player once it loads, so keyboard focus never falls back to the top of the page when the poster disappears.

  • Nothing auto-plays, so nothing needs a stop button

    Neither carousel engine Hero Banner uses ever advances on its own — slides change only on an explicit click on a control or indicator. WCAG 2.2.2's persistent pause control exists for autoplaying content; since nothing here autoplays, there's nothing to pause.

What this module can't enforce for you

  • Content quality

    Resolving alt text from the media item, or leaving it empty for a decorative image, still assumes whoever set up that media item wrote something meaningful — this module can require the value exists, not that it's meaningful. The same goes for rich HTML in props like Text's body, Statistic's body, or Hero Banner Slide's body: extra headings, links, and images authored inside them aren't validated either.

  • Colour contrast

    No component here names a colour — see the two-layer contract. Contrast is entirely the values a theme puts behind --theme-*, not something this module can guarantee on a theme's behalf.

When you set a custom theme's palette, check it against WCAG 2.1's contrast ratios: 4.5:1 for normal text, 3:1 for large text (24px+, or 19px+ bold) and for the graphical parts of UI components like button borders and focus indicators. --theme-color-primary-ink in the token reference exists specifically for this — it's a computed contrast colour with no Bootstrap equivalent, meant to be the text colour that reads clearly against --theme-color-primary at whatever value your theme gives it.

Before you ship a theme

  • Tab through it. Every interactive element a mouse reaches should be reachable and operable from the keyboard alone, in an order that matches what's on screen.
  • Turn on "reduce motion" in your OS accessibility settings and reload. Animations should stop; content shouldn't.
  • Check contrast on your actual token values, not the Bootstrap defaults — a custom theme that redeclares --theme-color-primary is also redeclaring what has to stay readable against it.