Das Pflicht-Fundament für jede Seite: Barrierefreiheit nach BFSG und
rücksichtsvolle Bewegung. Diese Bausteine gehören in jedes Projekt, bevor Optik draufkommt —
sichtbarer Fokus, Skip-Link, Screenreader-Texte, prefers-reduced-motion und
Touch-Komfort. Akzentfarbe über --akzent in einer Zeile anpassbar.
Skip-Link „Zum Inhalt springen"
Pol 2 · KlarheitAnlass: Tastatur-Navigation, jede Seite
Erster fokussierbarer Link der Seite — off-screen geparkt, springt bei :focus sichtbar herein und lässt Tastatur-Nutzer die Navigation überspringen (BFSG/WCAG 2.4.1). Vorbild: Bernstein-Montageservice. Demo: in die Box klicken, dann Tab drücken.
Pol 2 · KlarheitAnlass: alle interaktiven Elemente
Sichtbarer Tastatur-Fokus für Links, Buttons und Felder — Pflicht nach BFSG/WCAG 2.4.7. :focus-visible zeigt den Ring nur bei Tastatur, nicht bei Maus-Klick. Wichtig: auf dunklen oder farbigen Flächen einen hellen Ring setzen, sonst ist er unsichtbar (Demo: Tab durch die zwei Elemente). Vorbild: NDS.
<a href="#">Mehr erfahren</a>
<button type="button">Angebot anfordern</button>
<style>
/* Ein Ring für ALLE interaktiven Elemente */
:where(a,button,input,select,textarea,[tabindex]):focus-visible{
outline:3px solid var(--akzent, #7c87ff);
outline-offset:2px;
border-radius:8px}
/* Auf dunklen/farbigen Flaechen heller Ring, sonst unsichtbar */
.btn-primary:focus-visible,
.site-header a:focus-visible{outline-color:#f4f4f5}
</style>
Screenreader-Text (.sr-only)
Pol 2 · KlarheitAnlass: Icon-Buttons, versteckte Labels
Text nur für Screenreader, optisch unsichtbar — für Icon-Links ohne sichtbare Beschriftung oder erklärende Zusätze. Nicht display:none (das blendet auch für Screenreader aus). Vorbild: NDS. Demo: das Icon trägt das versteckte Label „Anrufen".
☎Anrufen← Icon-Link mit verstecktem Label „Anrufen"
Wer im System „Bewegung reduzieren" aktiviert hat, soll keine Animationen sehen (BFSG/WCAG 2.3.3, gegen Schwindel/Übelkeit). Statt Bewegung wegzulassen, einen ruhigen Ersatz-Zustand zeigen — hier endet das Quadrat statisch gedreht statt zu rotieren. Vorbild: Bernstein. Demo: aktiviere im System „Bewegung reduzieren", dann steht das Quadrat still.
Drei Mobil-Korrekturen: -webkit-tap-highlight-color entfernt den blauen Tap-Blitz, touch-action:manipulation killt die 300-ms-Doppeltipp-Verzögerung, und @media (hover:none) schaltet auf Touch-Geräten klebrige Hover-Zustände ab (sonst „hängt" der Hover nach dem Tippen fest). Vorbild: Silver-Mountain.
Hell/Dunkel-Umschalter ohne Flackern: ein winziges Inline-Skript im <head> setzt data-themevor dem ersten Paint. Die Wahl wird doppelt gespeichert (localStorage und Cookie) — falls eines blockiert ist, greift das andere. Button mit aria-pressed und Sonne/Mond-Tausch. Vorbild: NDS. Hinweis: Diese Demo schaltet nur die abgegrenzte Box um (data-fxtheme statt data-theme), damit sie nicht mit der Dossier-Seite kollidiert; der echte Code unten setzt data-theme auf <html>.
Vorschau
Küchenmontage
Klick auf das Symbol schaltet diese Box um.
<!-- 1) Inline-Bootstrap GANZ OBEN im <head>, vor dem CSS:
setzt data-theme vor dem ersten Paint = kein Flackern -->
<script>(function(){var t="dark";try{
var ls=localStorage.getItem("theme");
if(ls==="light"||ls==="dark"){t=ls;}
else{var m=document.cookie.match(/(?:^|;\s*)theme=(light|dark)/);if(m){t=m[1];}}
}catch(e){}document.documentElement.setAttribute("data-theme",t);})();</script>
<!-- 2) Toggle-Button im Header -->
<button type="button" class="theme-toggle" aria-pressed="false"
aria-label="Hellmodus aktivieren">
<svg class="icon-sun" ...></svg>
<svg class="icon-moon" ...></svg>
</button>
<style>
.theme-toggle .icon-moon{display:none}
[data-theme="light"] .theme-toggle .icon-sun{display:none}
[data-theme="light"] .theme-toggle .icon-moon{display:block}
[data-theme="light"]{ /* helle Variablen-Overrides */ }
</style>
<script>
(function(){
var btn=document.querySelector(".theme-toggle");
if(!btn) return;
function setLabel(t){
btn.setAttribute("aria-label", t==="light" ? "Dunkelmodus aktivieren" : "Hellmodus aktivieren");
btn.setAttribute("aria-pressed", t==="light" ? "true" : "false");
}
setLabel(document.documentElement.getAttribute("data-theme")||"dark");
btn.addEventListener("click",function(){
var next=document.documentElement.getAttribute("data-theme")==="light" ? "dark" : "light";
document.documentElement.setAttribute("data-theme",next);
/* doppelte Persistenz: localStorage UND Cookie */
try{localStorage.setItem("theme",next);}catch(e){}
try{document.cookie="theme="+next+";path=/;max-age=31536000;samesite=lax";}catch(e){}
setLabel(next);
});
})();
</script>
Event-Delegation data-action="fn:arg;fn2"
Pol 2 · KlarheitAnlass: CSP-Härtung, viele Klick-Ziele
Ein einziger Klick-Listener am document liest data-action aus und ruft die passende Funktion — ersetzt alle onclick-Inline-Handler (sauberer für Content-Security-Policy). Mehrere Aktionen per ;, Argument per :. Ein keydown-Trigger macht auch Nicht-Buttons (z. B. eine FAQ-Zeile) per Enter/Space bedienbar. Vorbild: Nördlicht (kompakte Demo).
Wird auch per Enter / Leertaste bedient? ▾
Ja — der keydown-Trigger ruft click() auf.
Ausgabe erscheint hier.
<button data-action="ping:Hallo">ping:Hallo</button>
<button data-action="ping:Welt;mark">ping:Welt;mark</button>
<div class="faq" role="button" tabindex="0" data-action="toggleFaq">Frage ▾</div>
<script>
/* EIN globaler Click-Listener parst data-action="fn:arg;fn2" */
document.addEventListener("click",function(evt){
var trigger=evt.target.closest("[data-action]");
if(!trigger) return;
if(trigger.tagName==="A") evt.preventDefault();
trigger.dataset.action.split(";").forEach(function(str){
var parts=str.split(":"), fn=parts[0], arg=parts[1];
switch(fn){
case "ping": ping(arg); break;
case "mark": mark(trigger); break;
case "toggleFaq":toggleFaq(trigger); break;
default: console.warn("Unbekannte data-action:",str);
}
});
});
/* Tastatur: Enter/Space auf Nicht-Button/Link-Elementen */
document.addEventListener("keydown",function(evt){
if(evt.key!=="Enter" && evt.key!==" ") return;
var trigger=evt.target.closest("[data-action]");
if(!trigger) return;
if(trigger.tagName==="A" || trigger.tagName==="BUTTON") return;
evt.preventDefault();
trigger.click();
});
</script>
aria-live Statusmeldung (Live-Region)
Pol 2 · KlarheitAnlass: „Gespeichert", Filter-Treffer, Formular-Feedback
Eine aria-live-Region liest Statusänderungen automatisch vor, ohne den Fokus zu verschieben — Pflicht für alles, was sich ohne Seiten-Neuladen ändert (BFSG/WCAG 4.1.3). polite wartet auf eine Sprechpause, assertive unterbricht sofort (nur für Fehler). Wichtig: die Region muss schon im HTML stehen, bevor Text hineinkommt. Demo: Buttons setzen die Meldung.
Noch keine Meldung.
Screenreader liest die neue Meldung vor — der Fokus bleibt beim Button.
<!-- Region steht LEER schon im HTML, bevor Text kommt -->
<p class="status" role="status" aria-live="polite"></p>
<button type="button" onclick="melde('Änderungen gespeichert.')">Speichern</button>
<script>
function melde(text){
var region=document.querySelector(".status");
if(!region) return;
region.textContent=text; // Textwechsel loest die Vorlesung aus
}
</script>
<style>
/* polite = wartet auf Sprechpause · assertive = unterbricht (nur Fehler) */
/* role="status" impliziert aria-live="polite" – beides schadet nicht */
.status{margin:0;font-size:.9rem;color:#e9e9ee}
</style>
Pol 2 · KlarheitAnlass: Kontaktformular, Pflichtfeld-Prüfung
Ein fehlerhaftes Feld darf nicht nur rot werden — Screenreader-Nutzer sehen die Farbe nicht. aria-invalid="true" meldet den Fehlerzustand, aria-describedby verknüpft das Feld mit dem Fehlertext (wird beim Fokus mit vorgelesen), und die Meldung selbst liegt in einer aria-live-Region (BFSG/WCAG 3.3.1). Demo: Feld leer lassen und „Prüfen" klicken.
Pol 2 · KlarheitAnlass: Seite folgt der OS-Vorgabe
Ohne jeden Umschalter: Die Seite übernimmt automatisch, ob das Betriebssystem auf Hell oder Dunkel steht (@media (prefers-color-scheme)). Reines CSS, kein JS, keine Speicherung. Unterschied zum manuellen Toggle (Baustein 6): hier entscheidet das System. Hinweis: Da die echte OS-Vorgabe hier nicht umschaltbar ist, ahmt die Demo beide Zustände per Knopf nach (data-fxscheme); der echte Code unten nutzt die @media-Abfrage.
OS-Vorgabe
Küchenmontage
Farben folgen automatisch der System-Einstellung.
aktiv: Dunkel
<body>
<h1>Küchenmontage</h1>
</body>
<style>
/* Standard = Dunkel */
:root{--bg:#16161a;--text:#e9e9ee;--linie:#26262c}
/* Steht das OS auf Hell, kippen die Farben automatisch */
@media (prefers-color-scheme: light){
:root{--bg:#f4f4f5;--text:#17171b;--linie:#d8d8df}
}
body{background:var(--bg);color:var(--text)}
</style>
Fokusfalle im Dialog (Focus-Trap)
Pol 1 · CraftAnlass: Modal, Cookie-Banner, Overlay-Menü
Ein offener Dialog muss den Tastatur-Fokus einfangen: Tab springt nicht hinter den Dialog auf die Seite, sondern zykelt zwischen den Dialog-Elementen; Esc schließt, und der Fokus kehrt danach auf den auslösenden Button zurück (BFSG/WCAG 2.1.2 „keine Tastaturfalle" im positiven Sinn). Demo: öffnen, dann mit Tab durchsteppen — der Fokus bleibt im Dialog.
Termin bestätigen
Tab bleibt im Dialog, Esc schließt, Fokus kehrt zurück.
<button type="button" class="modal-open">Dialog öffnen</button>
<div class="modal-wrap" role="presentation">
<div class="veil" data-close="1"></div>
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="t">
<button class="close" data-close="1" aria-label="Dialog schließen">✕</button>
<h2 id="t">Termin bestätigen</h2>
<button class="ok" data-close="1">Bestätigen</button>
<button class="cancel" data-close="1">Abbrechen</button>
</div>
</div>
<script>
(function(){
var host = document.querySelector(".modal-open").parentNode;
var opener = host.querySelector(".modal-open");
var wrap = host.querySelector(".modal-wrap");
var dialog = host.querySelector(".modal");
var vorher = null; // Fokus vor dem Öffnen merken
var SEL="a[href],button:not([disabled]),input,select,textarea,[tabindex]:not([tabindex='-1'])";
function fokusierbare(){ return Array.prototype.slice.call(dialog.querySelectorAll(SEL)); }
function open(){
vorher=document.activeElement;
wrap.classList.add("is-open");
var f=fokusierbare(); if(f.length) f[0].focus(); // Fokus in den Dialog
document.addEventListener("keydown", onKey);
}
function close(){
wrap.classList.remove("is-open");
document.removeEventListener("keydown", onKey);
if(vorher) vorher.focus(); // Fokus zurück zum Auslöser
}
function onKey(e){
if(e.key==="Escape"){ close(); return; }
if(e.key!=="Tab") return;
var f=fokusierbare(); if(!f.length) return;
var erste=f[0], letzte=f[f.length-1];
// Tab-Zyklus einfangen: vom letzten zum ersten, mit Shift umgekehrt
if(e.shiftKey && document.activeElement===erste){ e.preventDefault(); letzte.focus(); }
else if(!e.shiftKey && document.activeElement===letzte){ e.preventDefault(); erste.focus(); }
}
opener.addEventListener("click", open);
wrap.addEventListener("click", function(e){
if(e.target.closest("[data-close]")) close(); // Veil / Buttons schließen
});
})();
</script>
<style>
.modal-wrap{position:fixed;inset:0;display:none;place-items:center;z-index:1000}
.modal-wrap.is-open{display:grid}
.veil{position:absolute;inset:0;background:rgba(0,0,0,.55)}
.modal{position:relative}
.modal :where(button):focus-visible{outline:3px solid var(--akzent, #7c87ff);outline-offset:2px}
</style>