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.
Table of Contents
- 1. Why Hyvä themes come with an accessibility head start
- 2. Semantic HTML: Hyvä compared to Luma
- 3. Alpine.js toggles and missing focus management
- 4. Icon-only buttons without accessible names
- 5. Keyboard navigation and focus order
- 6. Forms, error messages, and ARIA live regions
- 7. Color contrast in the Tailwind configuration
- 8. Testing tools: axe, Lighthouse, and screen readers
- 9. A practical checklist for the Hyvä accessibility audit
- 10. Summary
- 11. FAQ
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.