Stop regressions before they get merged
Accessibility bugs found in a pull request cost minutes. Found later in an accessibility audit, they cost days. A fixed checklist for semantic elements, form labels, keyboard operability and focus management in code review prevents exactly this expensive rework and makes accessibility a permanent part of every merge, without noticeably slowing down review speed.
Table of Contents
- 1. Why accessibility belongs in code review
- 2. Checking for semantic HTML instead of div soup
- 3. Forms: labels, error messages and required fields
- 4. Keyboard operability as a mandatory check
- 5. Focus management for new interactive components
- 6. Using ARIA attributes correctly and sparingly
- 7. Integrating automated checks into CI/CD
- 8. The checklist inside the pull request workflow
- 9. Balancing thoroughness against review speed
- 10. Summary
- 11. FAQ
1. Why accessibility belongs in code review
Many teams treat accessibility as an end-of-project task, usually as an external audit shortly before launch. The problem with this workflow: if missing keyboard operability or a missing form label is only discovered weeks after the merge during an audit, the code is already in production, further features already build on top of it, and a one-line fix turns into a refactor with regression risk. An accessibility check in code review moves the bug to the cheapest point in the entire development cycle: right before the merge, while the author still has the context of the change in mind and the fix usually takes only a few minutes.
This principle is known as shift-left testing: quality assurance moves as early as possible into the development process instead of being appended as a separate step at the end. For Magento and Hyva projects there is also a legal dimension since the German Barrierefreiheitsstarkungsgesetz (BFSG) took effect in 2025: online stores with consumer business must meet WCAG 2.2 AA. A reviewer who checks new phtml templates and Alpine.js components against a fixed checklist prevents violations from accumulating in the codebase over months and eventually resulting in a single, expensive remediation sprint.
2. Checking for semantic HTML instead of div soup
The most common violation a reviewer should spot immediately is so-called div soup: interactive elements such as buttons, links or form fields are rebuilt out of <div> or <span> with an onclick handler instead of using the matching native element. Screen reader users lose all information that an element is even interactive, and keyboard users automatically lose focusable elements that can be triggered with Enter or Space. The reviewer check is easy to state: every element that triggers an action must be a <button>. Every element that navigates must be an <a href="...">.
Just as important are landmark elements such as <nav>, <main>, <header>, <footer>, and an unbroken heading hierarchy from <h1> to <h6> without skipped levels. Screen reader users jump directly between landmarks and headings with a keyboard shortcut, without reading the page linearly at all. If a new Hyva layout block is missing a <main> element, or a product page jumps from <h2> straight to <h4>, that exact navigation breaks. This is usually recognizable in a diff within seconds once the reviewer specifically looks for it.
<!-- BAD: div soup with no semantics, keyboard and screen reader users excluded -->
<div class="btn-primary" onclick="addToCart()">
Add to cart
</div>
<div class="product-nav">
<div onclick="showTab('description')">Description</div>
<div onclick="showTab('reviews')">Reviews</div>
</div>
<!-- GOOD: native, semantic elements with built-in keyboard operability -->
<button type="button" class="btn-primary" @click="addToCart()">
Add to cart
</button>
<nav class="product-nav" aria-label="Product details">
<button type="button" @click="activeTab = 'description'"
:aria-selected="activeTab === 'description'" role="tab">
Description
</button>
<button type="button" @click="activeTab = 'reviews'"
:aria-selected="activeTab === 'reviews'" role="tab">
Reviews
</button>
</nav>
3. Forms: labels, error messages and required fields
Forms are the area where accessibility bugs cost revenue most directly, because a user who cannot identify an input field abandons checkout. The most important reviewer check: every <input>, <select> and <textarea> needs an associated <label> via for/id, never just placeholder text. Placeholders disappear once typing starts, are not read aloud by some screen readers at all, and have too little contrast for users with low vision. A label, in contrast, stays permanently visible and is programmatically tied to the field.
Error messages are the second critical point: a red-colored border alone conveys no information to screen reader users. The error message must exist as text and be linked to the field via aria-describedby, and the field itself needs aria-invalid="true". Related radio buttons or checkboxes, for example a shipping method choice, belong inside a <fieldset> with a <legend>, so screen readers announce the group relationship. Required fields need both the native required attribute and a visible indicator, so sighted and non-sighted users receive the same information.
<!-- Accessible form field with label, required indicator and error message -->
<div class="form-group">
<label for="customer-email" class="block font-medium mb-1">
Email address <span aria-hidden="true">*</span>
<span class="sr-only">(required)</span>
</label>
<input
type="email"
id="customer-email"
name="email"
required
aria-required="true"
:aria-invalid="errors.email ? 'true' : 'false'"
aria-describedby="customer-email-error"
class="border rounded-lg px-3 py-2 w-full"
>
<p id="customer-email-error" class="text-red-600 text-sm mt-1" x-show="errors.email" role="alert">
Please enter a valid email address.
</p>
</div>
<!-- Related options grouped with fieldset/legend -->
<fieldset class="border rounded-lg p-4">
<legend class="font-medium px-2">Shipping method</legend>
<label class="flex items-center gap-2 mb-2">
<input type="radio" name="shipping" value="standard" checked>
Standard shipping (3-5 business days)
</label>
<label class="flex items-center gap-2">
<input type="radio" name="shipping" value="express">
Express shipping (1 business day)
</label>
</fieldset>
4. Keyboard operability as a mandatory check
The fastest accessibility check a reviewer can perform without any extra tool is the keyboard test: put the mouse aside, tab through the new component, trigger it with Enter or Space, close it with Escape. Every interactive element must be reachable and operable this way, in a logical order that matches the visual layout. The most common bug in custom-built components such as custom dropdowns: the element gets a @click handler, but neither tabindex="0" nor a @keydown handler for Enter and Space, leaving it completely unreachable for keyboard users.
A second classic that should stand out in review: outline: none or outline: 0 in CSS rules that remove the visible focus ring without defining an equivalent replacement. Without a visible focus indicator, a keyboard user loses orientation completely, because it is no longer clear which element is currently active. The modern, correct solution is :focus-visible, which shows the focus ring only for keyboard navigation and stays unobtrusive on mouse clicks, without the old JavaScript workaround previously needed to distinguish mouse from keyboard focus.
/* BAD: focus ring removed entirely, no alternative defined */
button:focus,
a:focus,
input:focus {
outline: none;
}
/* GOOD: focus ring only for keyboard navigation, clearly visible */
button:focus-visible,
a:focus-visible,
input:focus-visible,
[tabindex]:focus-visible {
outline: 2px solid #2563eb;
outline-offset: 2px;
border-radius: 4px;
}
/* Mouse click stays unobtrusive, no distracting ring on pointer devices */
button:focus:not(:focus-visible) {
outline: none;
}
/* Custom dropdown: visible state for keyboard operation */
.dropdown-trigger[aria-expanded="true"] {
outline: 2px solid #2563eb;
outline-offset: 2px;
}
5. Focus management for new interactive components
Modals, dropdown menus, off-canvas navigation and toast notifications are the component types where focus management bugs most often slip through a review unnoticed, because they work fine when tested with a mouse. When a modal opens, focus must move programmatically to the first interactive element inside it, otherwise keyboard focus stays invisibly somewhere in the background while a new dialog visually appears in the foreground. Inside the open modal, a focus trap must prevent Tab navigation from escaping the dialog into the hidden background.
Just as important, and even more often forgotten: when the modal closes, focus must return to the triggering element, usually the button that opened the modal. Without this return, keyboard focus lands at the top of the document after closing, and the user has to navigate through the entire page again. In Alpine.js components, used consistently throughout Hyva themes, this behavior can be cleanly encapsulated with x-init, $refs, and a simple keydown handler for Escape and Tab, without any additional JavaScript libraries.
// Alpine.js modal component with focus management and a focus trap
function accessibleModal() {
return {
open: false,
triggerElement: null,
openModal(event) {
// Remember the triggering element to restore focus later
this.triggerElement = event.currentTarget;
this.open = true;
this.$nextTick(() => {
// Move focus to the first interactive element inside the modal
this.$refs.modalPanel.querySelector('button, [href], input')?.focus();
});
},
closeModal() {
this.open = false;
// Return focus to the element that opened the modal
this.triggerElement?.focus();
},
trapFocus(event) {
if (event.key === 'Escape') {
this.closeModal();
return;
}
if (event.key !== 'Tab') return;
const focusable = this.$refs.modalPanel.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
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();
}
}
};
}
6. Using ARIA attributes correctly and sparingly
The first rule of ARIA is: no ARIA is better than bad ARIA. A <button> with role="button" is redundant but harmless. A <div> with role="button" that has neither tabindex nor a keydown handler, on the other hand, is more harmful than no ARIA attribute at all, because it pretends an interactivity to screen readers that does not actually exist. Reviewers should check every role attribute for whether a native HTML element with the same semantics already exists, one that also delivers keyboard operability for free.
Used well, ARIA attributes solve problems that HTML alone cannot cover: aria-expanded on an accordion or dropdown trigger communicates the open state, aria-live="polite" on a cart item counter ensures changes are announced automatically without moving focus, and aria-label supplies missing visible text on icon-only buttons. Important for review: aria-label completely overrides any visible text, which is why it should never be used on elements that already have meaningful visible text content, only when no visible text actually exists.
7. Integrating automated checks into CI/CD
Automated tools like axe-core reliably find roughly 30 to 50 percent of all accessibility bugs, mainly structural, machine-checkable problems such as missing labels, insufficient color contrast, or duplicate IDs. That percentage sounds low, but in practice it covers exactly the bugs that get introduced most often by accident and that a reviewer under time pressure is most likely to miss. Running axe-core against core templates as part of the CI pipeline prevents this class of bugs from ever reaching human review, giving the reviewer confidence to focus on the cases automation cannot detect.
For Hyva projects, pa11y-ci is a lightweight tool that checks rendered pages against a rule set and fails the build status on violations. It is important to curate the rules deliberately: not every WCAG rule is suited for a hard CI gate, and some produce false positives against dynamically generated Alpine.js markup. A pragmatic starting point limits itself to the highest-precision rules, such as missing labels, missing alt text, and insufficient contrast, and expands the rule set gradually once the team has gained confidence in the results.
{
"defaults": {
"timeout": 30000,
"wait": 500,
"standard": "WCAG2AA",
"runners": ["axe", "htmlcs"],
"ignore": [
"WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail"
]
},
"urls": [
"https://staging.mironsoft.de/",
"https://staging.mironsoft.de/checkout/cart/",
"https://staging.mironsoft.de/sample-product.html"
]
}
8. The checklist inside the pull request workflow
A checklist nobody reads adds no value. The most effective place for it is the pull request template itself, as a checkable list that appears for every change with UI impact. Four items cover most regressions: were semantic elements used instead of generic divs? Do all form fields have labels? Is the component fully keyboard operable? Is focus correctly set for new interactive elements (modal, dropdown, toast)? These four questions can be answered in under two minutes and cover the bug classes that automated tools are most likely to miss.
What matters is that the checklist becomes part of the definition of done rather than an optional recommendation gathering dust in a wiki. A reviewer who approves a pull request with a new interactive component without having run the keyboard test should be treated the same as a reviewer who approves without looking at test coverage: an incomplete review. Some teams additionally assign a rotating accessibility responsibility per sprint, so the check is not implicitly delegated to everyone and therefore to no one.
9. Balancing thoroughness against review speed
Running a full WCAG 2.2 audit against all 50 success criteria for every pull request is neither practical nor sensible, because most changes do not introduce any new interactive elements at all. The pragmatic approach is risk-based: a plain text change or a CSS color adjustment does not need a full keyboard test, but a new interactive component such as a filter dropdown or a modal does. This classification can be captured directly as a checkbox in the pull request template, so the scope of the check scales automatically with the risk of the change instead of being identical for every change.
Automation handles the cases with high precision and little room for interpretation, such as missing labels or contrast violations. What remains for the reviewer are the cases that require judgment: is an alt text actually meaningful or only technically present? Is the tab order logical even if it matches the DOM? This split keeps review fast without sacrificing thoroughness, because humans are only checked where machines fail, and the machine checks where human attention is most likely to slip.
| Check point | Without an accessibility checklist | With a checklist in the pull request | Effect |
|---|---|---|---|
| Semantic elements | Div soup only noticed during the audit | Button check in the diff, seconds per PR | No refactor after the merge |
| Form labels | Missing labels only noticed via a user complaint | label-for/id checked during review | Fewer checkout abandonments |
| Keyboard operability | Only tested with a mouse, never with Tab | Keyboard test as a mandatory step before approval | Component usable by everyone |
| Focus management | Modal without focus return ships to production | Focus trap check for new components | Orientation is preserved |
| Review speed | Full re-audit blocks the release | Risk-based check, automation handles routine cases | Merge pace stays stable |
Mironsoft
Accessibility, accessibility audits, and Hyva development for Magento stores
Ready to embed accessibility into your development process?
We set up accessibility checklists, PR templates, and automated axe-core and pa11y checks for your Magento and Hyva stack, so accessibility bugs surface before the merge instead of in an expensive later audit.
Accessibility Audit
WCAG 2.2 review of existing templates and components with prioritization
Review Process
PR checklists, definition of done, and reviewer training for your team
CI Integration
Adding and curating axe-core and pa11y-ci in your deploy pipeline
10. Summary
Accessibility in code review solves a structural problem: bugs that only surface in a later audit are expensive because production code has already been built on top of them by then. A fixed checklist with four core questions (semantic elements, form labels, keyboard operability, focus management) catches the most common regressions directly in the pull request, while the author still has the context in mind. Automated tools like axe-core and pa11y-ci handle the mechanically checkable cases with high precision, so the human reviewer can focus on judgment calls that machines cannot reliably answer.
The decisive lever for sustainable accessibility is not a single, thorough audit, but the consistent application of a lean checklist across every single pull request. A risk-based scope that adapts to the type of change keeps review fast without cutting corners on thoroughness for new interactive components. That keeps accessibility a permanent part of daily development instead of an isolated compliance project.
Accessibility in Code Review, the Essentials at a Glance
Semantics first
Native elements (button, a, nav, main) instead of div soup. Check landmark and heading hierarchy in the diff.
Labels & forms
Every field needs a label. Link error messages via aria-describedby, never placeholder text alone.
Keyboard & focus
Tab test without a mouse as a mandatory step. Modals need a focus trap and focus return on close.
Automation & balance
axe-core/pa11y-ci catch mechanical bugs. Risk-based scope keeps review fast.