Barrierefreiheit und Best Practices im neuen Frontend
Barrierefreiheit und Best Practices im neuen Frontend
~8 Min. Lesezeit Zuletzt aktualisiert am 9. August 2026
Block 6 schließt mit einer gezielten Durchsicht der vier Templates aus Kapitel 53 unter Barrierefreiheits-Gesichtspunkten. Kein neues Feature entsteht hier - jeder Punkt ist eine kleine, konkrete Korrektur oder Ergänzung an bereits gezeigtem Markup, mit Begründung statt bloßer Behauptung.
Semantische Tabelle statt Karten-Umbau
Kapitel 53 hat das bereits vorweggenommen: history/index.phtml nutzt eine echte <table> mit <caption class="sr-only">, <th scope="col"> pro Spaltenkopf und horizontalem Scrollen (overflow-x-auto) statt einer per CSS in Karten verwandelten Tabelle. Ein Screenreader-Nutzer, der per Tabellen-Navigation (z. B. NVDA/JAWS Tabellenmodus) durch die Historie geht, bekommt "Spalte 3 von 4, Punkte, +150" korrekt angesagt - bei einer visuell in Karten umgebauten Tabelle ohne scope-Attribute geht dieser Zusammenhang häufig verloren.
aria-current für den aktiven Navigationslink
Magento\Framework\View\Element\Html\Link\Current aus Kapitel 52 setzt bereits automatisch class="current", wenn der Link zur aktuellen Seite zeigt - das allein ist aber nur eine visuelle, keine programmatisch auswertbare Markierung. Das Hyvä-Navigations-Template ergänzt dafür aria-current="page" auf Basis genau dieser Klasse:
<a href="<?= $escaper->escapeUrl($link->getHref()) ?>"
<?= $link->isCurrent() ? 'aria-current="page"' : '' ?>
class="<?= $link->isCurrent() ? 'font-semibold text-brand-accent' : 'text-gray-600' ?>">
<?= $escaper->escapeHtml($link->getLabel()) ?>
</a>So weiß nicht nur ein sehender Nutzer visuell, welcher Menüpunkt aktiv ist, sondern auch ein Screenreader kündigt "Meine Punkte, aktuelle Seite" an. Ohne aria-current bleibt die aktive Markierung ein rein optisches Signal.
Formularfelder und Fehlermeldungen
- Das Filter-
<select>auscatalog/index.phtmlhat ein perfor/idverknüpftes<label>statt eines reinenplaceholder-artigen Texts - Pflicht, kein optionales Extra, sonst kündigt ein Screenreader das Feld ohne jede Beschriftung an. - Die "nicht genug Punkte"-Meldung in
catalog/view.phtmlträgtrole="status": ein Screenreader liest sie automatisch vor, sobald sie im DOM erscheint (etwa nach einem Filter-Wechsel), ohne dass der Fokus dorthin springen muss - für eine reine Informationsmeldung angemessener alsrole="alert", das unterbrechender wirkt und für echte Fehler reserviert bleiben sollte. - Erfolgs-/Fehlermeldungen nach dem Redeem-POST (Kapitel 50, über
messageManager) laufen durch Hyväs bereits vorhandeneMagento_Theme::html/messages.phtml- dort istrole="alert"für Fehlermeldungen bereits korrekt gesetzt, dieses Modul muss daran nichts ändern oder duplizieren.
Fokus-Reihenfolge und der Bestätigungsdialog
Der Alpine-Dialog aus Kapitel 53 (x-trap.noscroll, role="dialog", aria-modal="true", aria-labelledby) erfüllt die vier Mindestanforderungen an ein zugängliches Modal: Fokus wird beim Öffnen in den Dialog verschoben und darin gefangen, Escape schließt ihn, der Dialog ist per aria-labelledby auf seine eigene Überschrift benannt, und aria-modal="true" teilt unterstützender Technologie mit, dass der Rest der Seite währenddessen inert ist. Ein Punkt fehlt in der Kapitel-53-Version noch bewusst als Übung: Der Fokus sollte beim Schließen (Escape oder "Abbrechen") wieder auf den ursprünglichen "Jetzt einlösen"-Button zurückspringen, statt am Seitenanfang zu landen - x-trap.noscroll übernimmt das nicht automatisch für den Rückweg, nur für das Öffnen.
<div x-data="{ confirmOpen: false }" x-id="['redeem-trigger']">
<button type="button" :id="$id('redeem-trigger')" @click="confirmOpen = true">...</button>
<div x-show="confirmOpen" x-trap.noscroll="confirmOpen"
@keydown.escape.window="confirmOpen = false; $nextTick(() => document.getElementById($id('redeem-trigger')).focus())">
...
</div>
</div>Farbkontrast bei den Status-Badges
Achtung: Die Tier-Badge (bg-amber-100 text-amber-800) und die Punkte-Badge (bg-brand-accent/10 text-brand-accent-dim) aus Kapitel 53 sehen im Design-Mockup meist ausreichend kontrastreich aus - transparente Hintergründe wie /10 hängen aber vom darunterliegenden Seitenhintergrund ab. Vor dem produktiven Rollout unbedingt mit einem Kontrastprüfer (z. B. den Chrome-DevTools-Accessibility-Panel) gegen den tatsächlich gerenderten Hintergrund prüfen, ob mindestens die WCAG-2.1-AA-Mindestquote von 4,5:1 für Fließtext erreicht wird - eine reine Bewertung im Design-Tool reicht dafür nicht aus.
prefers-reduced-motion
Der Dialog nutzt bewusst nur x-show ohne x-transition-Animation - ein abruptes Ein-/Ausblenden ist für Nutzer mit vestibulären Störungen unproblematischer als eine Zoom- oder Fade-Animation. Würde eine Animation ergänzt, gehört @media (prefers-reduced-motion: reduce) zwingend dazu, entweder als Tailwind-motion-reduce:-Variante oder als eigene CSS-Regel im Theme, die transition-duration und animation-duration global auf nahezu 0 setzt.
Checkliste für Block 6
- Jede interaktive Fläche (Button, Link, Formularfeld) hat einen sichtbaren
focus-visible-Zustand - in allen vier Templates aus Kapitel 53 bereits überfocus-visible:outline-Utility-Klassen umgesetzt. - Jedes Bild und jedes bedeutungstragende Icon (in diesem Block gibt es aktuell keine reinen Icon-Buttons ohne Textlabel - sollte sich das in Block 7/9 mit Widget- oder Produkttyp-Icons ändern, braucht jedes davon ein
<span class="sr-only">oderaria-label). - Tabellen nutzen
<th scope>statt gestylter<td>-Zellen. - Modale Dialoge fangen den Fokus, schließen per Escape und bekommen ihn beim Schließen zurück.
- Statusmeldungen nutzen
role="status"für Information,role="alert"nur für echte Fehler. - Farbkontraste werden am gerenderten Ergebnis geprüft, nicht am Mockup.
Tipp: Damit ist Block 6 abgeschlossen: von der ersten sichtbaren Seite (Kapitel 45) über den eigenen Router (Kapitel 46), die Block/ViewModel-Entscheidung (Kapitel 47/48), den Prämienkatalog samt Einlösen (Kapitel 49/50) bis zur vollständigen, zugänglichen Frontend-Oberfläche (Kapitel 51-54). Block 7 baut darauf auf: ein "Meine Punkte"-Widget für CMS-Seiten und ein eigener Page-Builder-Content-Type - beide greifen auf die in diesem Block gebauten ViewModels zurück, statt sie zu duplizieren.