Hyvä Theme Accessibility: Fundamentals and Common Gaps
AI generated
A11Y
WCAG
Accessibility · Hyvä Theme · WCAG 2.2 · Magento 2
Hyvä Theme Accessibility: Fundamentals and Common Gaps
From a semantic head start to an accessible audit

Hyvä Theme's lean, semantic HTML foundation gives it noticeably better accessibility out of the box than classic Luma themes, but that doesn't close every gap automatically. Teams that ship Alpine.js toggles without focus management or icon buttons without accessible names still produce real WCAG violations. This article covers the strengths, the most common mistakes, and a practical checklist for your first Hyvä accessibility audit.

18 min. read Semantic HTML · ARIA · Focus Management Hyvä Theme · WCAG 2.2 · Alpine.js

1. Why Hyvä themes come with an accessibility head start

Anyone moving from a classic Luma theme to Hyvä Theme quickly notices that automated accessibility scans report noticeably fewer violations than before. That is not a coincidence, it comes from the architecture: Hyvä drops jQuery, Knockout.js, and the UI Component library entirely and instead renders server-side, semantic HTML with Tailwind CSS and Alpine.js for the few interactive parts. Where Luma often inflates interactive elements into <div> and <span> constructs wired up with Knockout bindings, Hyvä ships real <button>, <nav>, and <fieldset> elements out of the box.

This structural head start is real, but it is not a free pass. A cleanly rendered semantic skeleton does not automatically mean WCAG conformance, because many accessibility problems only appear once your own module development starts: missing focus management on Alpine.js-driven overlays, icon buttons without an accessible name, or form errors that are only visible, never announced to a screen reader. Since June 2025, Germany's Barrierefreiheitsstärkungsgesetz (BFSG) also obligates many German online stores to meet WCAG 2.1 Level AA, turning what used to be a nice-to-have into a legal requirement.

2. Semantic HTML: Hyvä compared to Luma

The difference shows up most clearly on simple controls. Luma templates frequently rely on <a href="#" data-role="action"> constructs that only get a click handler bolted on afterwards through a Knockout binding, without keyboard interaction or a role attribute coming along for free. Hyvä templates render a native <button type="button"> for the same function, reachable via Tab out of the box, responding to Enter and Space, and correctly announced as a button by screen readers, with zero extra ARIA required.

Landmark elements such as <header>, <nav>, <main>, and <footer> are already set correctly in the Hyvä base theme, letting screen reader users jump quickly between page regions. That does not relieve developers of their own diligence, though: as soon as a module adds its own components, <div onclick=""> creeps back in instead of <button>, or the heading hierarchy jumps from h2 straight to h4 because that font size looked better visually. The comparison below shows both variants for a quantity field in the cart.


<!-- BAD: div-based control, no keyboard access, no accessible role -->
<div class="qty-decrease" onclick="decreaseQty()">-</div>
<span class="qty-value">2</span>
<div class="qty-increase" onclick="increaseQty()">+</div>

<!-- GOOD: native Hyva pattern with real buttons and a labeled input -->
<div class="flex items-center gap-2" x-data="{ qty: 2 }">
    <button type="button"
            class="w-8 h-8 rounded border border-gray-300"
            aria-label="Decrease quantity"
            @click="qty = Math.max(1, qty - 1)">-</button>

    <label for="qty-input" class="sr-only">Product quantity</label>
    <input id="qty-input" type="number" min="1" x-model="qty"
           class="w-14 text-center border border-gray-300 rounded">

    <button type="button"
            class="w-8 h-8 rounded border border-gray-300"
            aria-label="Increase quantity"
            @click="qty++">+</button>
</div>

3. Alpine.js toggles and missing focus management

The most common finding in real Hyvä audits is not in the base theme, it is in custom-built components: mobile navigation, the mini-cart drawer, the product image lightbox, and the filter overlay are almost always shown and hidden with Alpine.js via x-show or x-if, correct for mouse users, but often a dead end for keyboard and screen reader users. When a drawer opens, keyboard focus typically stays on the trigger button while the visible content sits elsewhere in the DOM. Users keep tabbing blindly through the page, never realizing anything opened at all.

The correct pattern actively moves focus into the opened element, traps it there with a focus trap, and returns it to the trigger button on close. On top of that, aria-expanded belongs on the trigger button and aria-hidden="true" on the hidden panel, so the state is also communicated correctly to assistive technology. An @keydown.escape handler for closing is quick to retrofit with Alpine in Hyvä projects and is something many keyboard users expect by default.


// Reusable Alpine.js component: focus-trapped drawer with return focus
function accessibleDrawer() {
  return {
    open: false,
    triggerEl: null,

    openDrawer() {
      this.triggerEl = document.activeElement;
      this.open = true;
      this.$nextTick(() => {
        // Move focus into the drawer as soon as it is rendered
        const first = this.$refs.panel.querySelector('[href], button, input, [tabindex]');
        if (first) first.focus();
      });
    },

    closeDrawer() {
      this.open = false;
      // Return focus to the element that opened the drawer
      if (this.triggerEl) this.triggerEl.focus();
    },

    trapFocus(event) {
      const focusable = this.$refs.panel.querySelectorAll('[href], button, input, [tabindex]');
      const first = focusable[0];
      const last = focusable[focusable.length - 1];

      if (event.shiftKey && document.activeElement === first) {
        event.preventDefault();
        last.focus();
      } else if (!event.shiftKey && document.activeElement === last) {
        event.preventDefault();
        first.focus();
      }
    }
  };
}

4. Icon-only buttons without accessible names

Hyvä themes consistently favor lean SVG icons over icon fonts, a win for performance, but a common accessibility problem follows: the search icon, cart icon, wishlist heart, and close cross are often shipped as a bare <button><svg>...</svg></button>, with no visible text at all. A screen reader simply announces such an element as "button", with no information about what the button actually does. Sighted mouse users understand the icon from context, blind users have no such chance.

The fix is lightweight in Hyvä templates: either an aria-label directly on the button, or a visually hidden <span class="sr-only"> with descriptive text inside it. It also matters that the SVG itself is marked with aria-hidden="true" and focusable="false", so screen readers do not announce it twice or instead of the label. For buttons that change state, such as a wishlist heart toggling between "add" and "remove", the aria-label must also change dynamically with the state, otherwise the announcement goes stale.


<!-- BAD: icon-only button with no accessible name -->
<button type="button" class="p-2">
    <svg class="w-5 h-5" viewBox="0 0 24 24"><path d="..."/></svg>
</button>

<!-- GOOD: aria-label plus hidden svg, state-aware label -->
<button type="button"
        class="p-2"
        :aria-label="inWishlist ? 'Remove from wishlist' : 'Add to wishlist'"
        :aria-pressed="inWishlist"
        @click="toggleWishlist()">
    <svg class="w-5 h-5" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
        <path d="..."/>
    </svg>
</button>

<!-- Alternative: visually hidden text instead of aria-label -->
<button type="button" class="p-2" @click="closeModal()">
    <svg class="w-5 h-5" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
        <path d="..."/>
    </svg>
    <span class="sr-only">Close dialog</span>
</button>

5. Keyboard navigation and focus order

Because Hyvä renders server-side and keeps DOM manipulation to a minimum, the natural tab order stays intact in most cases, a structural advantage over complex single-page applications where focus order often has to be rebuilt manually. Problems typically appear where custom components abandon native semantics: sort dropdowns, color swatch pickers, or image carousels built with <div> and @click instead of native interactive elements are often unreachable by Tab and respond to neither Enter nor the arrow keys.

A second, underrated issue is the missing skip link. Many Hyvä installations ship with no "Skip to content" link at the top of the page by default, so keyboard users have to tab through the entire main navigation on every page load before they reach the actual page content. Just as important: visible focus indicators must not be removed with outline: none without defining an equivalent replacement. Tailwind's focus-visible: variant lets you show the ring specifically on keyboard focus and hide it on mouse clicks, instead of sacrificing focus visibility altogether.

6. Forms, error messages, and ARIA live regions

Checkout and contact forms in Hyvä stores are almost always equipped with Alpine.js for live validation: an error message appears via x-show="error" below the field as soon as the user types an invalid format. Visually that works well, but for screen reader users the newly shown text usually goes unnoticed, because it is inserted without an aria-live region. The screen reader only reads out what the user currently has focused, not what changes somewhere else on the page.

Two additions fix this reliably: the error text container gets aria-live="polite", so new content is announced automatically as soon as it appears. On top of that, aria-describedby links the input field to the ID of the error text, so the screen reader also reads the error message when the field itself receives focus. In addition, aria-invalid="true" on the input explicitly signals the erroneous state, independent of the color or position of the error text.


<!-- Accessible form field with live-announced validation error -->
<div x-data="{ error: '', touched: false }">
    <label for="email" class="block text-sm font-medium mb-1">Email address</label>
    <input
        id="email"
        type="email"
        name="email"
        :aria-invalid="error ? 'true' : 'false'"
        aria-describedby="email-error"
        @blur="touched = true; error = validateEmail($event.target.value)"
        class="w-full border border-gray-300 rounded px-3 py-2"
    >
    <!-- aria-live announces the error as soon as it appears -->
    <p id="email-error"
       x-show="touched && error"
       aria-live="polite"
       class="text-sm text-red-600 mt-1"
       x-text="error"></p>
</div>

7. Color contrast in the Tailwind configuration

Tailwind CSS makes it easy to define a custom color palette in seconds, and just as easy to accidentally introduce contrast problems while doing so. A common pattern in Hyvä stores: light gray placeholder text, subtle text-gray-400 metadata, or badge components with light text on a mid-tone gradient background. WCAG 2.1 AA requires a contrast ratio of at least 4.5:1 for normal body text, a lower bar for large text at 18pt or 14pt bold, and at least 3:1 for purely graphical UI components like focus rings or icon borders.

The most reliable way to keep this consistent over time is a documented color token system directly in the Tailwind configuration: instead of arbitrary gray tones from the default palette, define project-specific tokens with checked, code-commented contrast ratios. Tools like the WebAIM Contrast Checker or browser extensions for contrast checking can be built into the review process, so new color combinations get checked before merge instead of surfacing only in the finished audit.


/* Documented color tokens with checked contrast ratios (WCAG 2.1 AA) */
@theme {
  /* Body text on white background: ratio 7.5:1, passes AA and AAA */
  --color-text-primary: #27272a;

  /* Secondary text on white background: ratio 4.6:1, passes AA for normal text */
  --color-text-secondary: #52525b;

  /* Placeholder text: ratio 4.5:1, minimum AA threshold, do not go lighter */
  --color-text-placeholder: #71717a;

  /* Focus ring on white background: ratio 3.4:1, passes AA for UI components */
  --color-focus-ring: #3f3f46;

  /* Error text on white background: ratio 5.9:1, passes AA */
  --color-error: #b91c1c;
}

8. Testing tools: axe, Lighthouse, and screen readers

Automated tools are the fastest way into a Hyvä accessibility audit, but they only cover part of the WCAG criteria, estimates put it at roughly 30 to 40 percent. The axe DevTools browser extension checks a loaded page directly against the WCAG rule sets and flags missing labels, contrast failures, and invalid ARIA attributes with concrete locations in the DOM. Lighthouse delivers a fast accessibility score that works well as a regression signal in the CI pipeline, while WAVE marks issues directly and visually on the page, which is useful for communicating with non-technical colleagues.

No automated tool, however, detects whether a focus trap actually works, whether a screen reader's announcement order makes sense, or whether an Alpine.js toggle really returns focus correctly. That requires a manual keyboard pass through the entire checkout flow using only Tab, Shift+Tab, Enter, Space, and Escape, along with a screen reader smoke test using NVDA on Windows or VoiceOver on macOS or iOS across the most important page types: home page, product detail page, cart, and checkout.

Mironsoft

Accessibility audits, WCAG conformance, and Hyvä theme development

Ready to make your Hyvä store accessible and BFSG-compliant?

We audit your Hyvä store against WCAG 2.1 AA, identify focus management gaps, missing ARIA labels, and contrast failures, and implement the fixes directly in the theme, including a keyboard and screen reader test.

Accessibility audit

Automated scans plus a manual keyboard and screen reader test against WCAG 2.1 AA

Hyvä refactoring

Retrofitting focus management, ARIA labels, and form validation directly in the theme

BFSG consulting

Classifying legal requirements and prioritizing fixes by user impact

9. A practical checklist for the Hyvä accessibility audit

A structured audit does not start with individual color values, it starts with prioritization: check navigation and landmarks first, then test every Alpine.js-driven overlay for focus management, then check every icon button for an accessible name, followed by forms, and finally color contrast. This order reflects which mistakes block the largest number of users: a broken focus trap in checkout prevents a purchase entirely, while an overly light placeholder text makes things harder without blocking them outright.

The table below summarizes the five most common findings from real Hyvä projects, each with the typical mistake and the recommended pattern for fixing it.

Area Typical mistake Recommended pattern Effect
Icon buttons SVG without aria-label or sr-only text aria-label + aria-hidden on the SVG Screen reader announces the function correctly
Modal / drawer Focus stays on the trigger button Focus trap + return to trigger Keyboard users can reach the content
Form errors Error text is only visual, no live region aria-live="polite" + aria-describedby Error is announced automatically
Color contrast text-gray-400 on white background (about 2.8:1) Documented tokens at 4.5:1 or above Text stays readable for everyone
Skip link No "skip to content" link present Visible skip link as the first focus target Keyboard users can bypass the navigation

In practice, most findings trace back to the same root cause: a module was added without consistently carrying forward the existing Hyvä conventions for native elements, ARIA attributes, and focus management. Making the checklist a fixed part of code review catches most of these gaps before merge, instead of finding them only in the finished audit.

10. Summary

Hyvä theme accessibility starts with a genuine structural advantage: semantic HTML instead of div soup, native buttons instead of Knockout bindings, correct landmark elements out of the box. That head start does not replace diligence during ongoing development, though. The most common gaps appear where custom modules abandon the existing conventions: Alpine.js toggles without focus management, icon buttons without an accessible name, form errors without an aria-live region, and overly light color contrast in custom Tailwind palettes.

A structured audit that starts with navigation and focus management and works through icon buttons and forms toward color contrast surfaces the highest-impact problems first. Automated tools like axe and Lighthouse provide a fast initial overview, but never replace the manual keyboard and screen reader test. Since Germany's Barrierefreiheitsstärkungsgesetz took effect, WCAG 2.1 AA is no longer optional for many Magento stores either, it is a legal requirement.

Hyvä Theme Accessibility, the Essentials at a Glance

Semantic foundation

Native button, nav, fieldset instead of div soup. Hyvä ships landmarks and keyboard access in the base theme already.

Focus management

Alpine.js overlays need active focus movement, a focus trap, and returning focus to the trigger button on close.

Icon buttons & ARIA

aria-label or sr-only text on every icon button, aria-hidden on the SVG, dynamic labels on state change.

Testing & audit

axe and Lighthouse to get started, manual keyboard and screen reader testing for the truly critical gaps.

11. FAQ: Hyvä Theme Accessibility

1Is Hyvä Theme automatically WCAG-compliant?
No. Hyvä offers a much better base than Luma thanks to semantic HTML, but it doesn't replace your own audit. Custom modules can contain the same mistakes as in any other theme.
2What structurally sets Hyvä apart from Luma on accessibility?
Hyvä renders native elements like button, nav, and fieldset server-side, while Luma builds many interactive elements on div and span constructs via Knockout bindings.
3Why is focus management on Alpine.js toggles so important?
Without active focus movement, focus stays on the trigger button when a drawer opens, keyboard users cannot reach the new content.
4How do I make icon-only buttons accessible?
With aria-label or sr-only text on the button. The SVG itself gets aria-hidden and focusable false so nothing is announced twice.
5What is the difference between aria-hidden and sr-only?
aria-hidden hides an element from assistive technology while staying visually visible. sr-only hides visually but stays readable for screen readers. Both complement each other on icon buttons.
6Which WCAG level should a Magento store meet?
WCAG 2.1 Level AA, the standard target for commercial websites and the conformance level referenced in the BFSG.
7Is an automated scan with axe or Lighthouse enough?
No, automated tools cover only about 30 to 40 percent of the WCAG criteria. Focus traps and real usability require manual testing.
8How do I test with a screen reader without prior experience?
Use NVDA on Windows or VoiceOver on macOS/iOS. Walk through the home page, product page, cart, and checkout using only keyboard and screen reader.
9What is Germany's Barrierefreiheitsstärkungsgesetz (BFSG) about?
Since June 2025, the BFSG has required many German online stores to meet WCAG 2.1 AA, unless they fall under the small-business exemption.
10Where should I start with a Hyvä accessibility audit?
Prioritize by user impact: navigation and landmarks, then focus management, then icon buttons, forms, and finally color contrast.