use crate::output::report_model::{Effort, ExampleBlock, Role};
pub struct RuleExplanation {
pub customer_title: &'static str,
pub customer_title_en: &'static str,
pub customer_description: &'static str,
pub customer_description_en: &'static str,
pub user_impact: &'static str,
pub user_impact_en: &'static str,
pub typical_cause: &'static str,
pub typical_cause_en: &'static str,
pub recommendation: &'static str,
pub recommendation_en: &'static str,
pub technical_note: &'static str,
pub technical_note_en: &'static str,
pub responsible_role: Role,
pub effort_estimate: Effort,
pub example_bad: Option<&'static str>,
pub example_good: Option<&'static str>,
pub example_decorative: Option<&'static str>,
}
impl RuleExplanation {
pub fn examples(&self) -> Vec<ExampleBlock> {
match (self.example_bad, self.example_good) {
(Some(bad), Some(good)) => vec![ExampleBlock {
bad: bad.to_string(),
good: good.to_string(),
decorative: self.example_decorative.map(|s| s.to_string()),
}],
_ => vec![],
}
}
pub fn customer_title_for(&self, locale: &str) -> &'static str {
if locale == "en" {
self.customer_title_en
} else {
self.customer_title
}
}
pub fn customer_description_for(&self, locale: &str) -> &'static str {
if locale == "en" {
self.customer_description_en
} else {
self.customer_description
}
}
pub fn user_impact_for(&self, locale: &str) -> &'static str {
if locale == "en" {
self.user_impact_en
} else {
self.user_impact
}
}
pub fn typical_cause_for(&self, locale: &str) -> &'static str {
if locale == "en" {
self.typical_cause_en
} else {
self.typical_cause
}
}
pub fn recommendation_for(&self, locale: &str) -> &'static str {
if locale == "en" {
self.recommendation_en
} else {
self.recommendation
}
}
pub fn technical_note_for(&self, locale: &str) -> &'static str {
if locale == "en" {
self.technical_note_en
} else {
self.technical_note
}
}
}
pub fn get_explanation(rule_id: &str) -> Option<&'static RuleExplanation> {
if let Some(expl) = EXPLANATIONS
.iter()
.find(|(id, _)| *id == rule_id)
.map(|(_, e)| e)
{
return Some(expl);
}
if rule_id.contains('.') {
use crate::taxonomy::rules::RULES;
if let Some(rule) = RULES.iter().find(|r| r.id == rule_id) {
if let Some(ext_ref) = rule.external_ref {
let wcag_id = ext_ref.strip_prefix("WCAG ").unwrap_or(ext_ref);
return EXPLANATIONS
.iter()
.find(|(id, _)| *id == wcag_id)
.map(|(_, e)| e);
}
}
}
None
}
static EXPLANATIONS: &[(&str, RuleExplanation)] = &[
(
"1.1.1",
RuleExplanation {
customer_title: "Fehlende Alternativtexte bei Bildern",
customer_title_en: "Missing alternative text for images",
customer_description:
"Bilder auf der Website haben keinen beschreibenden Alternativtext. \
Dadurch können Screenreader den Bildinhalt nicht an blinde oder \
sehbeeinträchtigte Nutzer vermitteln.",
customer_description_en:
"Images on the website have no descriptive alternative text. \
As a result, screen readers cannot convey the image content to \
blind or visually impaired users.",
user_impact:
"Menschen mit Sehbeeinträchtigung erhalten an diesen Stellen keine oder \
nur unvollständige Information. Der Bildinhalt geht für sie vollständig verloren.",
user_impact_en:
"People with visual impairments receive no or only incomplete information \
at these points. The image content is entirely lost to them.",
typical_cause:
"Teaserbilder, Slider, redaktionell eingepflegte Medien ohne Pflichtfeld \
im CMS, oder dekorative Bilder, die nicht als solche markiert sind.",
typical_cause_en:
"Teaser images, sliders, or editorially uploaded media without a required \
field in the CMS, or decorative images that are not marked as such.",
recommendation:
"Für informative Bilder einen beschreibenden Alt-Text hinterlegen, der den \
Bildinhalt oder -zweck vermittelt. Rein dekorative Bilder mit einem leeren \
Alt-Attribut markieren (alt=\"\").",
recommendation_en:
"Provide a descriptive alt text for informative images that conveys the \
image content or purpose. Mark purely decorative images with an empty \
alt attribute (alt=\"\").",
technical_note:
"Informative Bilder: <img alt=\"Beschreibung\">. \
Dekorative Bilder: <img alt=\"\"> oder role=\"presentation\". \
CMS-Felder für Alt-Texte als Pflichtfeld konfigurieren.",
technical_note_en:
"Informative images: <img alt=\"description\">. \
Decorative images: <img alt=\"\"> or role=\"presentation\". \
Configure alt-text fields in the CMS as required.",
responsible_role: Role::Editorial,
effort_estimate: Effort::Quick,
example_bad: None,
example_good: None,
example_decorative: None,
},
),
(
"1.3.1",
RuleExplanation {
customer_title: "Fehlende semantische Struktur",
customer_title_en: "Missing semantic structure",
customer_description:
"Inhalte sind visuell strukturiert (z. B. durch Größe oder Farbe), aber \
die Struktur ist nicht im HTML-Code hinterlegt. Screenreader und andere \
Hilfstechnologien können die Beziehungen zwischen Inhalten nicht erkennen.",
customer_description_en:
"Content is visually structured (e.g. by size or color), but the structure \
is not encoded in the HTML. Screen readers and other assistive technologies \
cannot recognize the relationships between pieces of content.",
user_impact:
"Nutzer mit Screenreader können Tabellen, Listen und Formulargruppen nicht \
korrekt navigieren. Die logische Struktur der Seite geht verloren.",
user_impact_en:
"Screen reader users cannot navigate tables, lists, and form groups \
correctly. The logical structure of the page is lost.",
typical_cause:
"Tabellen ohne korrekte Tabellenauszeichnung, fehlende Fieldsets bei \
Formularen, Listen als div-Elemente statt ul/ol, fehlende Landmarks.",
typical_cause_en:
"Tables without proper table markup, missing fieldsets in forms, lists \
implemented as divs instead of ul/ol, missing landmarks.",
recommendation:
"Inhalte semantisch korrekt auszeichnen: Tabellen mit <table>, <th>, <td>; \
Listen mit <ul>/<ol>; Formulare mit <fieldset> und <legend> gruppieren.",
recommendation_en:
"Mark up content with correct semantics: tables with <table>, <th>, <td>; \
lists with <ul>/<ol>; group form fields with <fieldset> and <legend>.",
technical_note:
"HTML5-Semantik nutzen: <nav>, <main>, <aside>, <header>, <footer>. \
Tabellen: scope-Attribute für Kopfzellen. ARIA-Rollen nur als Ergänzung.",
technical_note_en:
"Use HTML5 semantics: <nav>, <main>, <aside>, <header>, <footer>. \
Tables: use scope attributes on header cells. Use ARIA roles only as a supplement.",
responsible_role: Role::Development,
effort_estimate: Effort::Medium,
example_bad: Some("<div class=\"table\">...</div>"),
example_good: Some("<table><thead><tr><th scope=\"col\">...</th></tr></thead>...</table>"),
example_decorative: None,
},
),
(
"a11y.presentation_semantic_children.invalid",
RuleExplanation {
customer_title: "ARIA-Rollen-Konflikt in Navigation",
customer_title_en: "ARIA role conflict in navigation",
customer_description:
"Ein Element ist mit role=\"none\" als semantisch leer markiert, \
enthält aber Kindelemente mit eigenen ARIA-Rollen. Das verwirrt \
Screenreader: Der Container hat keine Semantik, aber die enthaltenen \
Elemente referenzieren Rollentypen, die einen bestimmten Container \
voraussetzen.",
customer_description_en:
"An element is marked as semantically empty with role=\"none\", but \
contains child elements with their own ARIA roles. This confuses screen \
readers: the container has no semantics, but the contained elements \
reference role types that require a specific parent container.",
user_impact:
"Screenreader können Navigationsmenüs nicht korrekt interpretieren. \
Tastaturnutzer erhalten möglicherweise falsche oder fehlende \
Navigationshinweise.",
user_impact_en:
"Screen readers cannot correctly interpret navigation menus. Keyboard \
users may receive incorrect or missing navigation cues.",
typical_cause:
"Verwendung von ARIA-Mustern aus Desktop-Anwendungen (z. B. \
role=\"menuitem\") für Website-Navigation. role=\"none\" auf \
List-Elementen, die ARIA-Kinder mit Rollenanforderungen enthalten.",
typical_cause_en:
"Using ARIA patterns from desktop applications (e.g. role=\"menuitem\") \
for website navigation. role=\"none\" on list items that contain ARIA \
children with role requirements.",
recommendation:
"Website-Navigation ohne ARIA-Menubar-Muster aufbauen: \
<nav><ul><li><a> reicht für Screenreader aus. role=\"menuitem\" und \
role=\"menubar\" sind für Desktop-App-Menüs reserviert, nicht für \
Seitennavigation. role=\"none\" nur auf Elementen verwenden, die \
wirklich keine Kinder mit Eigenrollen haben.",
recommendation_en:
"Build website navigation without the ARIA Menubar pattern: \
<nav><ul><li><a> is sufficient for screen readers. role=\"menuitem\" \
and role=\"menubar\" are reserved for desktop application menus, not \
website navigation. Only use role=\"none\" on elements that truly have \
no children with their own roles.",
technical_note:
"ARIA APG: Navigation-Menubar ist für App-ähnliche Menüs mit \
Pfeilnavigation. Website-Navigation verwendet <nav> + natürliche \
Listenstruktur. role=\"none\" hebt nicht die Rollen von Nachfahren auf.",
technical_note_en:
"ARIA APG: Navigation Menubar is for app-like menus with arrow key \
navigation. Website navigation uses <nav> + natural list structure. \
role=\"none\" does not remove the roles of descendant elements.",
responsible_role: Role::Development,
effort_estimate: Effort::Quick,
example_bad: Some("<li role=\"none\"><a href=\"/home\" role=\"menuitem\">Home</a></li>"),
example_good: Some("<li><a href=\"/home\">Home</a></li>"),
example_decorative: None,
},
),
(
"1.3.5",
RuleExplanation {
customer_title: "Fehlende Eingabezweck-Kennzeichnung",
customer_title_en: "Missing input purpose identification",
customer_description:
"Formularfelder haben keine maschinenlesbare Kennzeichnung ihres Zwecks. \
Browser und Hilfstechnologien können dadurch keine Autofill-Vorschläge \
machen und Nutzern nicht helfen, Formulare schneller auszufüllen.",
customer_description_en:
"Form fields lack machine-readable identification of their purpose. \
As a result, browsers and assistive technologies cannot offer autofill \
suggestions or help users complete forms faster.",
user_impact:
"Menschen mit motorischen oder kognitiven Einschränkungen können nicht von \
automatischer Formularausfüllung profitieren. Das Ausfüllen dauert länger \
und ist fehleranfälliger.",
user_impact_en:
"People with motor or cognitive impairments cannot benefit from automatic \
form filling. Completing forms takes longer and is more error-prone.",
typical_cause:
"Fehlende autocomplete-Attribute in Formularen für persönliche Daten \
(Name, E-Mail, Adresse, Telefon).",
typical_cause_en:
"Missing autocomplete attributes on form fields for personal data \
(name, email, address, phone).",
recommendation:
"Alle Formularfelder für persönliche Daten mit dem passenden \
autocomplete-Attribut versehen (z. B. autocomplete=\"email\", \
autocomplete=\"given-name\").",
recommendation_en:
"Add the appropriate autocomplete attribute to all form fields for \
personal data (e.g. autocomplete=\"email\", autocomplete=\"given-name\").",
technical_note:
"autocomplete-Werte gemäß HTML-Spezifikation verwenden: \
name, email, tel, street-address, postal-code, country, etc.",
technical_note_en:
"Use autocomplete values per the HTML specification: \
name, email, tel, street-address, postal-code, country, etc.",
responsible_role: Role::Development,
effort_estimate: Effort::Quick,
example_bad: Some("<input type=\"email\" name=\"email\">"),
example_good: Some("<input type=\"email\" name=\"email\" autocomplete=\"email\">"),
example_decorative: None,
},
),
(
"1.4.1",
RuleExplanation {
customer_title: "Links nur durch Farbe erkennbar",
customer_title_en: "Links distinguishable by color alone",
customer_description:
"Links im Fließtext lassen sich ausschließlich durch ihre Farbe von normalem \
Text unterscheiden. Für Menschen mit Farbsehschwäche sind diese Links \
nicht als klickbar erkennbar.",
customer_description_en:
"Links within body text are distinguishable from regular text only by their \
color. For people with color vision deficiency, these links are not \
recognizable as clickable.",
user_impact:
"Nutzer mit Rot-Grün-Schwäche oder anderen Farbsehschwächen können Links \
im Text nicht erkennen und verpassen so wichtige Navigationsmöglichkeiten.",
user_impact_en:
"Users with red-green deficiency or other color vision impairments cannot \
recognize links in text and miss important navigation options.",
typical_cause:
"CSS setzt `text-decoration: none` auf Links im Fließtext ohne ein \
alternatives nicht-farbliches Unterscheidungsmerkmal wie Unterstrich, \
Fettschrift oder ein Icon.",
typical_cause_en:
"CSS sets `text-decoration: none` on inline links without an alternative \
non-color distinguishing feature such as underline, bold, or an icon.",
recommendation:
"Links im Fließtext mit einem nicht-farblichen Merkmal kennzeichnen: \
Unterstrich (Standard und empfohlen), Fettschrift oder ein kleines Icon. \
Der Unterstrich ist die stärkste Konvention.",
recommendation_en:
"Mark inline links with a non-color cue: underline (standard and recommended), \
bold text, or a small icon. Underline is the strongest convention.",
technical_note:
"CSS: `a { text-decoration: underline; }` oder bei `text-decoration: none` \
mindestens `font-weight: bold` oder `border-bottom`. \
Gilt nur für Links im Fließtext, nicht für Links in Navigationselementen.",
technical_note_en:
"CSS: `a { text-decoration: underline; }` or, if `text-decoration: none`, \
at least `font-weight: bold` or `border-bottom`. \
Applies only to inline links in body text, not to navigation links.",
responsible_role: Role::DesignUx,
effort_estimate: Effort::Quick,
example_bad: Some("a { color: #0057b8; text-decoration: none; }"),
example_good: Some("a { color: #0057b8; text-decoration: underline; }"),
example_decorative: None,
},
),
(
"1.4.3",
RuleExplanation {
customer_title: "Unzureichender Farbkontrast",
customer_title_en: "Insufficient color contrast",
customer_description:
"Text auf der Website hat nicht genügend Kontrast zum Hintergrund. \
Bei ungünstigen Lichtverhältnissen oder für Menschen mit \
Sehschwäche ist der Text schwer lesbar.",
customer_description_en:
"Text on the website has insufficient contrast against its background. \
Under poor lighting conditions or for people with low vision, the text \
is hard to read.",
user_impact:
"Menschen mit Sehbeeinträchtigung, ältere Nutzer und alle Nutzer bei \
ungünstigen Bildschirmbedingungen (Sonnenlicht, schlechte Displays) \
können Texte schlecht oder gar nicht lesen.",
user_impact_en:
"People with visual impairments, older users, and any user under poor \
screen conditions (sunlight, low-quality displays) can read the text \
only with difficulty or not at all.",
typical_cause:
"Helle Schriftfarbe auf hellem Hintergrund, graue Texte auf weißem Grund, \
Designentscheidungen ohne Kontrastprüfung.",
typical_cause_en:
"Light font color on a light background, gray text on white, \
design decisions made without contrast checking.",
recommendation:
"Schriftfarben anpassen, sodass normaler Text mindestens ein Kontrastverhältnis \
von 4,5:1 und großer Text von 3:1 zum Hintergrund hat. \
Kontrastwerte mit Tools wie dem WebAIM Contrast Checker prüfen.",
recommendation_en:
"Adjust font colors so normal text has a contrast ratio of at least 4.5:1 \
and large text at least 3:1 against the background. \
Verify contrast values with tools such as the WebAIM Contrast Checker.",
technical_note:
"WCAG 2.1 Level AA: 4.5:1 für normalen Text (<18pt / <14pt bold), \
3:1 für großen Text (≥18pt / ≥14pt bold). \
CSS-Variablen für konsistente Farbpalette verwenden.",
technical_note_en:
"WCAG 2.1 Level AA: 4.5:1 for normal text (<18pt / <14pt bold), \
3:1 for large text (≥18pt / ≥14pt bold). \
Use CSS variables for a consistent color palette.",
responsible_role: Role::DesignUx,
effort_estimate: Effort::Medium,
example_bad: Some("color: #999999; background: #ffffff; /* contrast 2.8:1 */"),
example_good: Some("color: #595959; background: #ffffff; /* contrast 7:1 */"),
example_decorative: None,
},
),
(
"1.4.4",
RuleExplanation {
customer_title: "Text nicht ausreichend skalierbar",
customer_title_en: "Text cannot be sufficiently resized",
customer_description:
"Texte auf der Website können nicht ohne Verlust von Inhalt oder Funktion \
auf 200% vergrößert werden. Nutzer, die auf größere Schrift angewiesen sind, \
verlieren dadurch Inhalte.",
customer_description_en:
"Text on the website cannot be enlarged to 200% without loss of content \
or functionality. Users who depend on larger text lose information.",
user_impact:
"Menschen mit Sehbeeinträchtigung, die die Schriftgröße im Browser vergrößern, \
stoßen auf abgeschnittene Texte, überlagerte Elemente oder nicht \
scrollbare Bereiche.",
user_impact_en:
"People with visual impairments who increase the browser font size \
encounter clipped text, overlapping elements, or non-scrollable areas.",
typical_cause:
"Feste Pixelwerte für Schriftgrößen und Container, overflow: hidden \
bei Textcontainern, fehlende responsive Anpassungen.",
typical_cause_en:
"Fixed pixel values for font sizes and containers, overflow: hidden on \
text containers, missing responsive adjustments.",
recommendation:
"Schriftgrößen in relativen Einheiten (rem/em) angeben. Container so gestalten, \
dass sie bei vergrößertem Text mitwachsen. Browser-Zoom auf 200% testen.",
recommendation_en:
"Specify font sizes in relative units (rem/em). Design containers so they \
grow with enlarged text. Test browser zoom at 200%.",
technical_note:
"font-size in rem/em statt px. Container mit min-height statt height. \
overflow: auto statt hidden. @media-Queries für verschiedene Zoom-Stufen.",
technical_note_en:
"Use font-size in rem/em instead of px. Use min-height on containers \
instead of height. Prefer overflow: auto over hidden. Use @media queries \
for different zoom levels.",
responsible_role: Role::Development,
effort_estimate: Effort::Medium,
example_bad: Some("font-size: 12px; height: 40px; overflow: hidden;"),
example_good: Some("font-size: 0.75rem; min-height: 2.5rem; overflow: auto;"),
example_decorative: None,
},
),
(
"1.4.10",
RuleExplanation {
customer_title: "Kein automatischer Textumbruch (Reflow)",
customer_title_en: "No automatic text wrapping (Reflow)",
customer_description:
"Die Website passt sich bei starker Vergrößerung oder schmalen Bildschirmen \
(320 CSS-Pixeln) nicht flexibel an. Nutzer müssen horizontal scrollen, um Text zu lesen.",
customer_description_en:
"The website does not adapt flexibly to high zoom levels or small screens \
(320 CSS pixels). Users have to scroll horizontally to read the text.",
user_impact:
"Menschen mit Sehbehinderungen, die stark zoomen (bis 400%), müssen \
in zwei Dimensionen scrollen. Das Lesen langer Texte wird extrem mühsam oder unmöglich.",
user_impact_en:
"People with visual impairments who zoom in heavily (up to 400%) must scroll \
in two directions. Reading text becomes extremely tedious or impossible.",
typical_cause:
"Verwendung von festen Pixelbreiten (width: 960px) oder overflow: hidden, \
was den Umbruch von Texten verhindert.",
typical_cause_en:
"Using fixed pixel widths (width: 960px) or overflow: hidden, which \
prevents text from wrapping properly.",
recommendation:
"Nutze responsives Design mit relativen Einheiten (%, vw, rem). Vermeide feste \
Breiten auf Containern, die Text enthalten. Teste Zoom auf 400%.",
recommendation_en:
"Use responsive design with relative units (%, vw, rem). Avoid fixed widths \
on containers holding text. Test zooming up to 400%.",
technical_note:
"WCAG 2.1 Level AA: No loss of content or function at width 320px (horizontal scroll \
must be avoided for text-flow containers). Use max-width: 100% and flexbox/grid.",
technical_note_en:
"WCAG 2.1 Level AA: No loss of content or function at width 320px (horizontal scroll \
must be avoided for text-flow containers). Use max-width: 100% and flexbox/grid.",
responsible_role: Role::Development,
effort_estimate: Effort::Medium,
example_bad: Some("div.container { width: 960px; }"),
example_good: Some("div.container { max-width: 100%; width: 100%; }"),
example_decorative: None,
},
),
(
"1.4.11",
RuleExplanation {
customer_title: "Unzureichender Kontrast bei UI-Elementen",
customer_title_en: "Insufficient contrast on UI elements",
customer_description:
"Bedienelemente wie Buttons, Eingabefelder oder Icons haben nicht genügend \
Kontrast zum Hintergrund. Sie sind dadurch für manche Nutzer schwer erkennbar.",
customer_description_en:
"Interactive elements such as buttons, input fields, or icons have \
insufficient contrast against the background. They are therefore hard \
to recognize for some users.",
user_impact:
"Nutzer mit eingeschränktem Sehvermögen können interaktive Elemente nicht \
zuverlässig erkennen und bedienen.",
user_impact_en:
"Users with limited vision cannot reliably perceive and operate \
interactive elements.",
typical_cause:
"Subtile Rahmenfarben bei Formularelementen, Icons mit geringem Kontrast, \
Fokus-Indikatoren, die zu unauffällig sind.",
typical_cause_en:
"Subtle border colors on form elements, low-contrast icons, focus \
indicators that are too inconspicuous.",
recommendation:
"Alle interaktiven Elemente und ihre Zustände (Normal, Hover, Fokus) mit \
mindestens 3:1 Kontrast zum Hintergrund gestalten.",
recommendation_en:
"Design all interactive elements and their states (default, hover, focus) \
with a contrast ratio of at least 3:1 against the background.",
technical_note:
"3:1 Kontrastverhältnis für UI-Komponenten und grafische Objekte. \
Gilt für Rahmen, Icons, Slider, Checkboxen, etc.",
technical_note_en:
"3:1 contrast ratio for UI components and graphical objects. \
Applies to borders, icons, sliders, checkboxes, etc.",
responsible_role: Role::DesignUx,
effort_estimate: Effort::Medium,
example_bad: Some("border: 1px solid #cccccc; /* auf #ffffff = 1.6:1 */"),
example_good: Some("border: 1px solid #767676; /* auf #ffffff = 4.5:1 */"),
example_decorative: None,
},
),
(
"1.4.13",
RuleExplanation {
customer_title: "Fehlende Barrierefreiheit bei Zusatzinhalten (Tooltips)",
customer_title_en: "Suboptimal or inaccessible hover/focus content (tooltips)",
customer_description:
"Zusatzinhalte, die erst bei Hover (Mauszeiger) oder Fokus (Tastatur) erscheinen \
(wie Tooltips oder native title-Attribute), sind nicht barrierefrei zugänglich oder unvollständig verknüpft.",
customer_description_en:
"Additional content appearing on hover or focus (such as tooltips or native title attributes) \
is not accessible or not properly linked in the HTML code.",
user_impact:
"Nutzer von Screenreadern hören diese Inhalte gar nicht, Tastaturnutzer können sie oft nicht schließen, \
und auf Touchgeräten (Mobilgeräten) sind sie in der Regel unbrauchbar.",
user_impact_en:
"Screen reader users do not hear tooltips, keyboard users cannot dismiss them, \
and they are often completely unusable on touch devices.",
typical_cause:
"Verwendung des nativen HTML title-Attributs als einzige Beschriftung, unvollständige \
Zuweisung per aria-describedby oder fehlende Tastaturbedienbarkeit bei Custom-Tooltips.",
typical_cause_en:
"Using the native HTML title attribute for important information, missing aria-describedby \
associations, or lack of keyboard control on custom tooltips.",
recommendation:
"Verwende eine sichtbare Beschriftung oder moderne, barrierefreie Tooltips (schließbar mit Escape, \
verknüpft über aria-describedby und hoverbar).",
recommendation_en:
"Use visible labels or modern, accessible tooltips (dismissible with Escape, linked via \
aria-describedby, and hoverable).",
technical_note:
"Zusatzinhalte müssen (1) hoverbar sein (Maus kann in den Tooltip bewegt werden), (2) dismissible \
sein (Esc-Taste blendet ihn aus), (3) persistent sein. Tooltips mit role=\"tooltip\" per aria-describedby verknüpfen.",
technical_note_en:
"Additional content must be (1) hoverable, (2) dismissible (Esc), and (3) persistent. \
Associate tooltips carrying role=\"tooltip\" with their trigger via aria-describedby.",
responsible_role: Role::Development,
effort_estimate: Effort::Medium,
example_bad: Some("<button title=\"Mehr Info\">...</button>\n<div role=\"tooltip\">Hilfetext</div>"),
example_good: Some(
"<button aria-describedby=\"tip1\">Mehr Info</button>\n<div id=\"tip1\" role=\"tooltip\">Hilfetext</div>"
),
example_decorative: None,
},
),
(
"2.1.1",
RuleExplanation {
customer_title: "Inhalte nicht per Tastatur bedienbar",
customer_title_en: "Content not operable by keyboard",
customer_description:
"Bestimmte Funktionen der Website können nur mit der Maus bedient werden. \
Nutzer, die auf die Tastatur angewiesen sind, können diese Inhalte \
nicht erreichen oder nutzen.",
customer_description_en:
"Certain features of the website can only be operated with a mouse. \
Users who rely on the keyboard cannot reach or use these features.",
user_impact:
"Menschen mit motorischen Einschränkungen, die keine Maus verwenden können, \
sowie Screenreader-Nutzer sind von diesen Funktionen ausgeschlossen.",
user_impact_en:
"People with motor impairments who cannot use a mouse, as well as \
screen reader users, are excluded from these features.",
typical_cause:
"Click-Handler nur auf div/span statt auf interaktive Elemente, \
fehlende tabindex-Attribute, JavaScript-Widgets ohne Tastaturunterstützung.",
typical_cause_en:
"Click handlers attached to div/span instead of interactive elements, \
missing tabindex attributes, JavaScript widgets without keyboard support.",
recommendation:
"Alle interaktiven Elemente mit nativen HTML-Elementen (button, a, input) \
umsetzen. Bei Custom-Widgets Tastaturnavigation (Tab, Enter, Escape, Pfeiltasten) \
implementieren.",
recommendation_en:
"Implement all interactive elements with native HTML elements \
(button, a, input). For custom widgets, implement keyboard navigation \
(Tab, Enter, Escape, arrow keys).",
technical_note:
"Native HTML-Elemente bevorzugen. Bei Custom-Widgets: tabindex=\"0\", \
keydown/keyup-Handler, ARIA-Rollen und -Zustände.",
technical_note_en:
"Prefer native HTML elements. For custom widgets: tabindex=\"0\", \
keydown/keyup handlers, ARIA roles and states.",
responsible_role: Role::Development,
effort_estimate: Effort::Medium,
example_bad: Some("<div onclick=\"doAction()\">Klick mich</div>"),
example_good: Some("<button type=\"button\" onclick=\"doAction()\">Klick mich</button>"),
example_decorative: None,
},
),
(
"2.1.2",
RuleExplanation {
customer_title: "Tastaturfalle — Fokus kann nicht verlassen werden",
customer_title_en: "Keyboard trap — focus cannot leave the element",
customer_description:
"An bestimmten Stellen der Website bleibt der Tastaturfokus hängen. \
Nutzer können mit der Tastatur nicht mehr vor- oder zurücknavigieren.",
customer_description_en:
"At certain points on the website, keyboard focus gets stuck. \
Users can no longer navigate forward or backward with the keyboard.",
user_impact:
"Tastaturnutzer sind an dieser Stelle gefangen und können den Rest \
der Seite nicht mehr erreichen — die Seite wird faktisch unbenutzbar.",
user_impact_en:
"Keyboard users are trapped at this point and cannot reach the rest of \
the page — the page effectively becomes unusable.",
typical_cause:
"Modale Dialoge ohne korrekte Fokus-Verwaltung, Widgets, die den Fokus \
abfangen, fehlende Escape-Taste zum Verlassen.",
typical_cause_en:
"Modal dialogs without proper focus management, widgets that capture \
focus, missing Escape key to dismiss.",
recommendation:
"Sicherstellen, dass der Fokus jedes Element mit Tab und Shift+Tab \
verlassen kann. Modale Dialoge mit Escape schließbar machen und den \
Fokus danach korrekt zurücksetzen.",
recommendation_en:
"Ensure focus can leave every element using Tab and Shift+Tab. \
Make modal dialogs dismissible with Escape and restore focus correctly \
afterwards.",
technical_note:
"Focus trapping nur in Modals mit korrekter Implementierung. \
Escape zum Schließen. Fokus-Rückgabe an das auslösende Element.",
technical_note_en:
"Use focus trapping only in modals with a correct implementation. \
Allow Escape to close. Return focus to the triggering element.",
responsible_role: Role::Development,
effort_estimate: Effort::Medium,
example_bad: None,
example_good: None,
example_decorative: None,
},
),
(
"2.4.1",
RuleExplanation {
customer_title: "Fehlende Sprungnavigation",
customer_title_en: "Missing skip navigation",
customer_description:
"Die Website bietet keinen Mechanismus, um wiederkehrende Inhaltsblöcke \
(z. B. Navigation, Header) zu überspringen und direkt zum Hauptinhalt \
zu gelangen.",
customer_description_en:
"The website provides no mechanism to skip recurring blocks of content \
(e.g. navigation, header) and jump directly to the main content.",
user_impact:
"Tastaturnutzer und Screenreader-Nutzer müssen bei jedem Seitenwechsel \
erneut durch die gesamte Navigation tabben, bevor sie den Inhalt erreichen.",
user_impact_en:
"Keyboard and screen reader users must tab through the entire navigation \
again on every page change before they reach the content.",
typical_cause:
"Fehlender Skip-Link als erstes Element der Seite. \
WCAG 2.4.1 verlangt einen Mechanismus zum Überspringen wiederkehrender Blöcke — \
nicht zwingend Landmarks, aber ein sichtbarer Skip-Link ist die direkteste Lösung.",
typical_cause_en:
"Missing skip link as the first element on the page. \
WCAG 2.4.1 requires a mechanism to bypass repeated blocks — \
not necessarily landmarks, but a visible skip link is the most direct solution.",
recommendation:
"Einen sichtbaren Skip-Link ('Zum Inhalt springen') als erstes interaktives Element \
der Seite einbauen. HTML5-Landmarks (<nav>, <main>) sind ergänzend sinnvoll, \
ersetzen aber den Skip-Link nicht.",
recommendation_en:
"Add a visible skip link ('Skip to main content') as the first interactive \
element on the page. HTML5 landmarks (<nav>, <main>) are a useful addition \
but do not replace the skip link.",
technical_note:
"Skip-Link: <a href=\"#main\" class=\"skip-link\">Zum Inhalt springen</a>. \
Bei Fokus sichtbar machen via CSS :focus { clip: auto; position: static; }. \
Landmarks (<nav>, <main id=\"main\">) verbessern zusätzlich die Screenreader-Navigation.",
technical_note_en:
"Skip link: <a href=\"#main\" class=\"skip-link\">Skip to main content</a>. \
Make it visible on focus via CSS :focus { clip: auto; position: static; }. \
Landmarks (<nav>, <main id=\"main\">) additionally improve screen reader navigation.",
responsible_role: Role::Development,
effort_estimate: Effort::Quick,
example_bad: Some("<body><div class=\"nav\">...</div><div class=\"content\">...</div></body>"),
example_good: Some("<body><a href=\"#main\" class=\"skip-link\">Zum Inhalt</a><nav>...</nav><main id=\"main\">...</main></body>"),
example_decorative: None,
},
),
(
"2.4.2",
RuleExplanation {
customer_title: "Fehlender oder unzureichender Seitentitel",
customer_title_en: "Missing or insufficient page title",
customer_description:
"Die Seite hat keinen aussagekräftigen Titel im Browser-Tab. \
Nutzer können nicht erkennen, auf welcher Seite sie sich befinden.",
customer_description_en:
"The page has no meaningful title in the browser tab. \
Users cannot tell which page they are on.",
user_impact:
"Screenreader-Nutzer hören den Seitentitel als Erstes — ohne klaren Titel \
fehlt die Orientierung. Auch bei vielen offenen Tabs ist die Seite \
nicht zuzuordnen.",
user_impact_en:
"Screen reader users hear the page title first — without a clear title, \
they have no orientation. With many tabs open, the page is also hard \
to identify.",
typical_cause:
"Leeres <title>-Tag, generischer Titel ('Home', 'Untitled'), \
identischer Titel auf allen Seiten.",
typical_cause_en:
"Empty <title> tag, generic title ('Home', 'Untitled'), \
identical title on every page.",
recommendation:
"Jeder Seite einen eindeutigen, beschreibenden Titel geben, der den \
Seiteninhalt und die Website-Zugehörigkeit klar macht \
(z. B. 'Kontakt — Firmenname').",
recommendation_en:
"Give each page a unique, descriptive title that clearly states the page \
content and site context (e.g. 'Contact — Company Name').",
technical_note:
"<title>Seiteninhalt — Website</title>. Titel im CMS als Pflichtfeld. \
Pattern: Seitenspezifisch + Seitenname.",
technical_note_en:
"<title>Page content — Site</title>. Make the title a required field in \
the CMS. Pattern: page-specific + site name.",
responsible_role: Role::Editorial,
effort_estimate: Effort::Quick,
example_bad: Some("<title>Home</title>"),
example_good: Some("<title>Kontakt — Casoon Digital Solutions</title>"),
example_decorative: None,
},
),
(
"2.4.3",
RuleExplanation {
customer_title: "Unlogische Fokus-Reihenfolge",
customer_title_en: "Illogical focus order",
customer_description:
"Die Reihenfolge, in der interaktive Elemente per Tastatur erreicht werden, \
entspricht nicht der visuellen oder logischen Reihenfolge der Seite.",
customer_description_en:
"The order in which interactive elements are reached via keyboard does \
not match the visual or logical order of the page.",
user_impact:
"Tastaturnutzer erleben eine verwirrende Navigation — der Fokus springt \
zwischen unzusammenhängenden Bereichen hin und her.",
user_impact_en:
"Keyboard users experience confusing navigation — focus jumps back and \
forth between unrelated areas.",
typical_cause:
"Visuelles Layout per CSS umgeordnet ohne DOM-Reihenfolge anzupassen, \
positive tabindex-Werte, dynamisch eingefügte Elemente an falscher Stelle.",
typical_cause_en:
"Visual layout reordered via CSS without adjusting DOM order, positive \
tabindex values, dynamically inserted elements placed in the wrong spot.",
recommendation:
"Die DOM-Reihenfolge an die visuelle Reihenfolge anpassen. \
Auf positive tabindex-Werte verzichten. Bei dynamischen Inhalten \
den Fokus programmatisch steuern.",
recommendation_en:
"Align DOM order with the visual order. Avoid positive tabindex values. \
Manage focus programmatically for dynamic content.",
technical_note:
"tabindex nur 0 oder -1 verwenden. CSS-Order nicht für inhaltlich \
relevante Umordnungen nutzen. DOM-Reihenfolge = Lesereihenfolge.",
technical_note_en:
"Use only tabindex 0 or -1. Do not use CSS order for content-relevant \
reordering. DOM order = reading order.",
responsible_role: Role::Development,
effort_estimate: Effort::Medium,
example_bad: None,
example_good: None,
example_decorative: None,
},
),
(
"2.4.4",
RuleExplanation {
customer_title: "Unklare oder generische Linktexte",
customer_title_en: "Unclear or generic link text",
customer_description:
"Links auf der Seite haben Texte wie 'hier', 'mehr', 'weiterlesen', \
die ohne Kontext nicht verständlich sind. Nutzer können nicht erkennen, \
wohin ein Link führt.",
customer_description_en:
"Links on the page use text like 'here', 'more', 'read more' that is \
not understandable out of context. Users cannot tell where a link leads.",
user_impact:
"Screenreader-Nutzer navigieren häufig über eine Linkliste — generische \
Texte wie 'mehr' sind dort völlig nichtssagend. Auch für alle anderen \
Nutzer ist die Orientierung erschwert.",
user_impact_en:
"Screen reader users often navigate via a list of links — generic text \
like 'more' is meaningless there. Orientation is also harder for all \
other users.",
typical_cause:
"Redaktionelle Gewohnheit, 'hier klicken' oder 'mehr erfahren' als Linktext \
zu verwenden. Teaser-Komponenten mit generischem 'Weiterlesen'.",
typical_cause_en:
"Editorial habit of using 'click here' or 'learn more' as link text. \
Teaser components with a generic 'Read more'.",
recommendation:
"Linktexte so formulieren, dass sie auch ohne umgebenden Text verständlich \
sind (z. B. 'Leistungen im Bereich Webentwicklung' statt 'mehr erfahren').",
recommendation_en:
"Phrase link text so it is understandable without surrounding context \
(e.g. 'Web development services' instead of 'learn more').",
technical_note:
"Sprechende Linktexte verwenden. Falls visuell knapper Text gewünscht: \
aria-label oder aria-labelledby für erweiterte Beschreibung.",
technical_note_en:
"Use descriptive link text. If a visually short text is required, use \
aria-label or aria-labelledby for an extended description.",
responsible_role: Role::Editorial,
effort_estimate: Effort::Quick,
example_bad: Some("<a href=\"/leistungen\">mehr erfahren</a>"),
example_good: Some("<a href=\"/leistungen\">Unsere Leistungen im Bereich Webentwicklung</a>"),
example_decorative: None,
},
),
(
"2.4.6",
RuleExplanation {
customer_title: "Fehlende oder unzureichende Überschriftenstruktur",
customer_title_en: "Missing or insufficient heading structure",
customer_description:
"Die Seite verwendet keine oder unlogische Überschriften. \
Die inhaltliche Gliederung ist für assistive Technologien nicht erkennbar.",
customer_description_en:
"The page uses no headings or has an illogical heading structure. \
The content outline is not recognizable to assistive technologies.",
user_impact:
"Screenreader-Nutzer können nicht per Überschrift durch die Seite navigieren. \
Die Seite wird zu einem undifferenzierten Textblock — schnelles Finden \
relevanter Abschnitte ist unmöglich.",
user_impact_en:
"Screen reader users cannot navigate the page by headings. \
The page becomes an undifferentiated block of text — quickly finding \
relevant sections is impossible.",
typical_cause:
"Keine Überschriften verwendet, nur visuelle Formatierung (fett, groß). \
Überschriftenhierarchie übersprungen (z. B. H1 → H3). \
Mehrere oder fehlende H1.",
typical_cause_en:
"No headings used, only visual formatting (bold, large). \
Heading hierarchy skipped (e.g. H1 → H3). \
Multiple or missing H1.",
recommendation:
"Eine logische Überschriftenhierarchie aufbauen: genau eine H1 pro Seite, \
darunter H2 für Hauptabschnitte, H3 für Unterabschnitte. \
Keine Ebenen überspringen.",
recommendation_en:
"Build a logical heading hierarchy: exactly one H1 per page, H2 for \
main sections, H3 for sub-sections. Do not skip levels.",
technical_note:
"H1 = Seitentitel (einmal). H2-H6 hierarchisch verschachtelt. \
Keine Ebenen überspringen. Heading-Outline mit Browser-Extension prüfen.",
technical_note_en:
"H1 = page title (once). H2–H6 nested hierarchically. \
Do not skip levels. Verify the heading outline with a browser extension.",
responsible_role: Role::Editorial,
effort_estimate: Effort::Quick,
example_bad: Some("<div class=\"big-title\">Heading</div>"),
example_good: Some("<h1>Page title</h1>\n<h2>Section</h2>\n<h3>Subsection</h3>"),
example_decorative: None,
},
),
(
"2.4.7",
RuleExplanation {
customer_title: "Fokus-Indikator nicht sichtbar",
customer_title_en: "Focus indicator not visible",
customer_description:
"Beim Navigieren mit der Tastatur ist nicht erkennbar, welches Element \
gerade den Fokus hat. Der visuelle Fokus-Rahmen fehlt oder ist unsichtbar.",
customer_description_en:
"When navigating with the keyboard, it is not visible which element \
currently has focus. The visual focus outline is missing or invisible.",
user_impact:
"Tastaturnutzer verlieren die Orientierung — sie wissen nicht, wo sie sich \
auf der Seite befinden und welches Element sie gerade aktivieren würden.",
user_impact_en:
"Keyboard users lose orientation — they cannot tell where they are on \
the page or which element they would activate.",
typical_cause:
"CSS-Reset entfernt outline (outline: none / outline: 0). \
Kein eigener Fokus-Stil definiert. Fokus-Stil zu unauffällig.",
typical_cause_en:
"CSS reset removes the outline (outline: none / outline: 0). \
No custom focus style defined. Focus style too inconspicuous.",
recommendation:
"Einen gut sichtbaren Fokus-Indikator für alle interaktiven Elemente gestalten. \
Mindestens 2px Umrandung mit ausreichendem Kontrast.",
recommendation_en:
"Design a clearly visible focus indicator for all interactive elements. \
At least a 2px outline with sufficient contrast.",
technical_note:
":focus-visible statt :focus für Tastatur-only Fokus. \
outline: 2px solid #005fcc; outline-offset: 2px; \
Niemals outline: none ohne Alternative.",
technical_note_en:
"Use :focus-visible instead of :focus for keyboard-only focus. \
outline: 2px solid #005fcc; outline-offset: 2px; \
Never use outline: none without an alternative.",
responsible_role: Role::DesignUx,
effort_estimate: Effort::Quick,
example_bad: Some("*:focus { outline: none; }"),
example_good: Some("*:focus-visible { outline: 2px solid #005fcc; outline-offset: 2px; }"),
example_decorative: None,
},
),
(
"2.4.10",
RuleExplanation {
customer_title: "Fehlende Abschnittsüberschriften",
customer_title_en: "Missing section headings",
customer_description:
"Längere Inhalte sind nicht durch Abschnittsüberschriften gegliedert. \
Die Seite wirkt als ein zusammenhängender Block ohne erkennbare Struktur.",
customer_description_en:
"Long content is not broken up by section headings. \
The page reads as one continuous block with no recognizable structure.",
user_impact:
"Nutzer können Inhalte nicht gezielt ansteuern und müssen lange Textpassagen \
komplett durchlesen, um relevante Informationen zu finden.",
user_impact_en:
"Users cannot jump to specific content and have to read long passages in \
full to find relevant information.",
typical_cause:
"Lange Inhaltsseiten ohne Zwischenüberschriften. Redaktionelle Inhalte \
als Fließtext ohne Gliederung.",
typical_cause_en:
"Long content pages without sub-headings. Editorial content presented as \
running text with no structure.",
recommendation:
"Längere Inhalte mit aussagekräftigen Zwischenüberschriften gliedern. \
Alle 2-3 Absätze eine Überschrift einsetzen.",
recommendation_en:
"Break up long content with meaningful sub-headings. Add a heading every \
2–3 paragraphs.",
technical_note:
"Korrekte Heading-Hierarchie (H2, H3...) innerhalb von Abschnitten. \
WAI-ARIA: role=\"heading\" nur als Fallback, native HTML bevorzugen.",
technical_note_en:
"Use correct heading hierarchy (H2, H3...) within sections. \
WAI-ARIA: use role=\"heading\" only as a fallback; prefer native HTML.",
responsible_role: Role::Editorial,
effort_estimate: Effort::Quick,
example_bad: None,
example_good: None,
example_decorative: None,
},
),
(
"2.5.3",
RuleExplanation {
customer_title: "Sichtbarer Text stimmt nicht mit dem zugänglichen Namen überein",
customer_title_en: "Visible text does not match the accessible name",
customer_description:
"Der sichtbare Text eines Bedienelements stimmt nicht mit dem \
Namen überein, den assistive Technologien vorlesen. \
Sprachsteuerungsnutzer können das Element nicht ansprechen.",
customer_description_en:
"The visible text of a control does not match the name announced by \
assistive technologies. Voice-control users cannot address the element.",
user_impact:
"Nutzer, die Sprachsteuerung verwenden, können Elemente nicht per \
Sprachbefehl aktivieren, weil der vorgelesene Name nicht dem \
sichtbaren Text entspricht.",
user_impact_en:
"Users relying on voice control cannot activate elements by voice \
command because the announced name does not match the visible text.",
typical_cause:
"aria-label überschreibt sichtbaren Text mit abweichendem Wortlaut. \
Sichtbarer Text und zugänglicher Name sind unterschiedlich formuliert.",
typical_cause_en:
"aria-label overrides the visible text with different wording. \
Visible text and accessible name are phrased differently.",
recommendation:
"Sicherstellen, dass der zugängliche Name (aria-label) den sichtbaren \
Text enthält oder identisch ist.",
recommendation_en:
"Make sure the accessible name (aria-label) contains the visible text \
or is identical to it.",
technical_note:
"aria-label muss den sichtbaren Text als Teilstring enthalten. \
Besser: aria-label ganz weglassen, wenn der sichtbare Text ausreicht.",
technical_note_en:
"aria-label must include the visible text as a substring. \
Better: omit aria-label entirely when the visible text is sufficient.",
responsible_role: Role::Development,
effort_estimate: Effort::Quick,
example_bad: Some("<button aria-label=\"Close menu\">X</button>"),
example_good: Some("<button aria-label=\"Close\">Close</button>"),
example_decorative: None,
},
),
(
"3.1.1",
RuleExplanation {
customer_title: "Fehlende Sprachangabe der Seite",
customer_title_en: "Missing page language declaration",
customer_description:
"Die Hauptsprache der Seite ist nicht im HTML-Code angegeben. \
Screenreader und Übersetzungstools können die Sprache nicht \
automatisch erkennen.",
customer_description_en:
"The primary language of the page is not declared in the HTML. \
Screen readers and translation tools cannot detect the language \
automatically.",
user_impact:
"Screenreader lesen den Text mit falscher Aussprache vor — \
deutsche Inhalte werden z. B. mit englischer Phonetik gelesen, \
was unverständlich ist.",
user_impact_en:
"Screen readers read out the text with the wrong pronunciation — \
e.g. German content is read with English phonetics, which is \
unintelligible.",
typical_cause:
"Fehlendes lang-Attribut im <html>-Tag. Häufig bei Templates vergessen.",
typical_cause_en:
"Missing lang attribute on the <html> tag. Often forgotten in templates.",
recommendation:
"Das lang-Attribut im <html>-Tag setzen (z. B. lang=\"de\" für Deutsch). \
Bei mehrsprachigen Seiten zusätzlich Abschnitte mit lang-Attribut \
kennzeichnen.",
recommendation_en:
"Set the lang attribute on the <html> tag (e.g. lang=\"en\" for English). \
On multilingual pages, additionally mark sections with a lang attribute.",
technical_note:
"<html lang=\"de\"> für deutschsprachige Seiten. \
ISO 639-1 Sprachcodes verwenden. \
Für fremdsprachige Abschnitte: <span lang=\"en\">...</span>.",
technical_note_en:
"<html lang=\"en\"> for English-language pages. \
Use ISO 639-1 language codes. \
For foreign-language sections: <span lang=\"en\">...</span>.",
responsible_role: Role::Development,
effort_estimate: Effort::Quick,
example_bad: Some("<html>"),
example_good: Some("<html lang=\"de\">"),
example_decorative: None,
},
),
(
"3.2.1",
RuleExplanation {
customer_title: "Unerwartete Kontextänderung bei Fokus",
customer_title_en: "Unexpected change of context on focus",
customer_description:
"Wenn ein Element den Tastaturfokus erhält, ändert sich unerwartet \
der Kontext (z. B. Seite wird gewechselt, neues Fenster öffnet sich).",
customer_description_en:
"When an element receives keyboard focus, the context changes \
unexpectedly (e.g. the page changes, a new window opens).",
user_impact:
"Tastaturnutzer und Screenreader-Nutzer erleben unvorhersehbare \
Seitenveränderungen. Das ist verwirrend und stört den Arbeitsfluss.",
user_impact_en:
"Keyboard and screen reader users experience unpredictable page changes. \
This is confusing and disrupts their workflow.",
typical_cause:
"JavaScript-Events auf onfocus, die Navigation oder DOM-Änderungen auslösen. \
Select-Elemente, die bei Fokus bereits eine Aktion auslösen.",
typical_cause_en:
"JavaScript events on onfocus that trigger navigation or DOM changes. \
Select elements that perform an action on focus.",
recommendation:
"Kontextänderungen nur durch explizite Benutzeraktionen auslösen \
(Klick, Enter), nicht durch bloßen Fokuswechsel.",
recommendation_en:
"Trigger context changes only via explicit user actions (click, Enter), \
not by focus change alone.",
technical_note:
"Keine onchange/onfocus-Handler für Navigation. \
Select-Menüs mit Submit-Button statt auto-submit.",
technical_note_en:
"No onchange/onfocus handlers for navigation. \
Use a submit button with select menus instead of auto-submit.",
responsible_role: Role::Development,
effort_estimate: Effort::Quick,
example_bad: None,
example_good: None,
example_decorative: None,
},
),
(
"3.2.2",
RuleExplanation {
customer_title: "Unerwartete Kontextänderung bei Eingabe",
customer_title_en: "Unexpected change of context on input",
customer_description:
"Wenn ein Nutzer Eingaben in ein Formularelement macht oder eine \
Auswahl trifft, ändert sich unerwartet der Kontext der Seite.",
customer_description_en:
"When a user enters data into a form element or makes a selection, \
the page context changes unexpectedly.",
user_impact:
"Nutzer erleben verwirrende Änderungen beim Ausfüllen von Formularen. \
Besonders für Screenreader-Nutzer sind unangekündigte Änderungen problematisch.",
user_impact_en:
"Users experience confusing changes while filling out forms. \
Unannounced changes are especially problematic for screen reader users.",
typical_cause:
"Auto-Submit bei Formularänderungen. Seitenweiterleitung bei \
Select-Auswahl ohne Bestätigung.",
typical_cause_en:
"Auto-submit on form changes. Page redirect on select choice without \
confirmation.",
recommendation:
"Formularänderungen erst nach expliziter Bestätigung (Submit-Button) \
verarbeiten. Vorab ankündigen, wenn eine Eingabe sofortige Änderungen auslöst.",
recommendation_en:
"Process form changes only after an explicit confirmation (submit button). \
Announce up front when an input triggers immediate changes.",
technical_note:
"Kein auto-submit bei onchange. Explizite Submit-Buttons verwenden. \
Alternativ: ARIA-Live-Region für Vorab-Hinweis.",
technical_note_en:
"No auto-submit on onchange. Use explicit submit buttons. \
Alternatively: an ARIA live region for an advance notice.",
responsible_role: Role::Development,
effort_estimate: Effort::Quick,
example_bad: None,
example_good: None,
example_decorative: None,
},
),
(
"3.3.2",
RuleExplanation {
customer_title: "Fehlende Beschriftungen und Anleitungen bei Formularen",
customer_title_en: "Missing labels and instructions on forms",
customer_description:
"Formularfelder haben keine sichtbare Beschriftung oder Anleitung. \
Nutzer wissen nicht, welche Eingaben erwartet werden.",
customer_description_en:
"Form fields have no visible label or instruction. \
Users do not know what input is expected.",
user_impact:
"Alle Nutzer, besonders Menschen mit kognitiven Einschränkungen und \
Screenreader-Nutzer, können Formulare nicht korrekt ausfüllen.",
user_impact_en:
"All users, especially people with cognitive impairments and screen \
reader users, cannot complete forms correctly.",
typical_cause:
"Fehlende <label>-Elemente, nur Platzhaltertext statt sichtbarer Labels, \
fehlende Hinweise auf Pflichtfelder oder Eingabeformat.",
typical_cause_en:
"Missing <label> elements, placeholder text used instead of visible labels, \
no indication of required fields or expected input format.",
recommendation:
"Jedes Formularfeld mit einem sichtbaren, per <label> verknüpften \
Label versehen. Pflichtfelder kennzeichnen. Bei speziellen Formaten \
(z. B. Datum) das erwartete Format angeben.",
recommendation_en:
"Provide every form field with a visible label linked via <label>. \
Mark required fields. For special formats (e.g. date), state the \
expected format.",
technical_note:
"<label for=\"id\"> für alle Formularfelder. \
Pflichtfelder: required + aria-required=\"true\". \
Platzhalter nicht als einzige Beschriftung verwenden.",
technical_note_en:
"<label for=\"id\"> on all form fields. \
Required fields: required + aria-required=\"true\". \
Do not use placeholders as the only label.",
responsible_role: Role::Development,
effort_estimate: Effort::Quick,
example_bad: Some("<input type=\"text\" placeholder=\"Name\">"),
example_good: Some("<label for=\"name\">Name *</label>\n<input type=\"text\" id=\"name\" required aria-required=\"true\">"),
example_decorative: None,
},
),
(
"4.1.2",
RuleExplanation {
customer_title: "Fehlende Name/Rolle bei Bedienelementen",
customer_title_en: "Missing name/role on controls",
customer_description:
"Interaktive Elemente (Buttons, Links, Formularfelder) haben keinen \
zugänglichen Namen oder keine erkennbare Rolle. Assistive Technologien \
können nicht vermitteln, worum es sich handelt.",
customer_description_en:
"Interactive elements (buttons, links, form fields) have no accessible \
name or recognizable role. Assistive technologies cannot convey what \
the element is.",
user_impact:
"Screenreader-Nutzer hören z. B. nur 'Button' ohne Beschreibung der Funktion, \
oder ein klickbares Element wird gar nicht als interaktiv erkannt.",
user_impact_en:
"Screen reader users hear, for example, only 'button' with no description \
of its function, or a clickable element is not recognized as interactive \
at all.",
typical_cause:
"Buttons oder Links ohne Text oder aria-label. Icon-only-Buttons ohne \
zugänglichen Namen. Custom-Widgets ohne ARIA-Rollen.",
typical_cause_en:
"Buttons or links with no text or aria-label. Icon-only buttons without \
an accessible name. Custom widgets without ARIA roles.",
recommendation:
"Alle interaktiven Elemente mit einem verständlichen, zugänglichen Namen \
versehen. Native HTML-Elemente bevorzugen. Bei Icon-Buttons: \
aria-label verwenden.",
recommendation_en:
"Give every interactive element a clear, accessible name. Prefer native \
HTML elements. For icon buttons: use aria-label.",
technical_note:
"Buttons: sichtbarer Text oder aria-label. \
Links: sprechender Linktext. \
Inputs: verknüpftes <label>. \
Custom-Widgets: role + aria-label + aria-Zustände.",
technical_note_en:
"Buttons: visible text or aria-label. \
Links: descriptive link text. \
Inputs: associated <label>. \
Custom widgets: role + aria-label + ARIA states.",
responsible_role: Role::Development,
effort_estimate: Effort::Quick,
example_bad: Some("<button><svg>...</svg></button>"),
example_good: Some("<button aria-label=\"Open menu\"><svg aria-hidden=\"true\">...</svg></button>"),
example_decorative: None,
},
),
(
"a11y.aria_hidden_focus.invalid",
RuleExplanation {
customer_title: "Tastaturfokus auf unsichtbaren Elementen",
customer_title_en: "Keyboard focus on hidden elements",
customer_description:
"Ein interaktives Element ist über aria-hidden=\"true\" vor Screenreadern versteckt, \
kann aber dennoch mit der Tastatur (Tabulator) fokussiert werden.",
customer_description_en:
"An interactive element is hidden from screen readers via aria-hidden=\"true\", \
but can still be focused using the keyboard (tab key).",
user_impact:
"Tastaturnutzer steuern auf unsichtbare Elemente (Fokus-Stolperfallen), \
während Screenreader-Nutzer keinerlei Rückmeldung über dieses Element erhalten.",
user_impact_en:
"Keyboard users navigate to hidden elements (focus traps), \
while screen reader users receive no feedback about the element.",
typical_cause:
"Es wurde aria-hidden=\"true\" verwendet, um ein Element visuell auszublenden, \
ohne tabindex=\"-1\" zu setzen oder das Element per display:none zu verstecken.",
typical_cause_en:
"Using aria-hidden=\"true\" to hide an element visually without setting \
tabindex=\"-1\" or hiding the element via display:none.",
recommendation:
"Wenn ein Element vollständig versteckt sein soll, display:none oder visibility:hidden verwenden. \
Soll es nur vor Screenreadern versteckt sein, muss es unfokussierbar sein (tabindex=\"-1\").",
recommendation_en:
"To hide an element completely, use display:none or visibility:hidden. \
If it should only be hidden from screen readers, make it unfocusable (tabindex=\"-1\").",
technical_note:
"Focusable elements (a, button, input) inside an aria-hidden=\"true\" tree must \
either be removed from tab order or hidden completely.",
technical_note_en:
"Focusable elements (a, button, input) inside an aria-hidden=\"true\" tree must \
either be removed from tab order or hidden completely.",
responsible_role: Role::Development,
effort_estimate: Effort::Quick,
example_bad: Some("<button aria-hidden=\"true\">Menu</button>"),
example_good: Some("<button aria-hidden=\"true\" tabindex=\"-1\">Menu</button>"),
example_decorative: None,
},
),
(
"a11y.aria_prohibited_attr.invalid",
RuleExplanation {
customer_title: "Ungültige oder verbotene ARIA-Attribute",
customer_title_en: "Prohibited ARIA attributes",
customer_description:
"Für bestimmte HTML-Elemente oder ARIA-Rollen werden Attribute verwendet, die für \
diese Rolle nicht zulässig oder sinnvoll sind.",
customer_description_en:
"Attributes are used on HTML elements or ARIA roles that are not permitted or \
meaningful for that specific role.",
user_impact:
"Screenreader erhalten widersprüchliche oder fehlerhafte Signale, was die \
Bedienbarkeit der Website beeinträchtigen kann.",
user_impact_en:
"Screen readers receive conflicting or incorrect markup signals, which can \
impair accessibility.",
typical_cause:
"Verwendung von ARIA-Attributen (wie aria-label oder aria-expanded) auf Elementen, \
die keine interaktive Rolle haben (z. B. span oder div ohne role).",
typical_cause_en:
"Using ARIA attributes (like aria-label or aria-expanded) on elements that \
do not have an interactive role (e.g. span or div without role).",
recommendation:
"Prüfe die ARIA-Spezifikation für die jeweilige Rolle. Verwende ARIA-Attribute \
nur auf passenden Elementen und Rollen.",
recommendation_en:
"Check the ARIA specification for the respective role. Only use ARIA attributes \
on appropriate elements and roles.",
technical_note:
"Prohibited attributes on generic elements: <span aria-label=\"text\"> is invalid. \
Use native elements or valid roles (e.g. role=\"button\").",
technical_note_en:
"Prohibited attributes on generic elements: <span aria-label=\"text\"> is invalid. \
Use native elements or valid roles (e.g. role=\"button\").",
responsible_role: Role::Development,
effort_estimate: Effort::Quick,
example_bad: Some("<span aria-label=\"Menu\">...</span>"),
example_good: Some("<button aria-label=\"Menu\">...</button>"),
example_decorative: None,
},
),
(
"a11y.modern_attributes.invalid",
RuleExplanation {
customer_title: "Fehlerhafte Dialog- oder Popover-Steuerung",
customer_title_en: "Broken dialog or popover control",
customer_description:
"Moderne HTML-Attribute für Popover, Dialoge oder gesperrte Bereiche sind \
unvollständig oder widersprüchlich eingesetzt.",
customer_description_en:
"Modern HTML attributes for popovers, dialogs, or disabled regions are used \
incompletely or inconsistently.",
user_impact:
"Tastatur- und Screenreader-Nutzer können Oberflächen öffnen, fokussieren oder \
verlassen, ohne eine verständliche Rückmeldung zu erhalten.",
user_impact_en:
"Keyboard and screen reader users may open, focus, or leave surfaces without \
understandable feedback.",
typical_cause:
"popovertarget verweist auf ein fehlendes Element, ein sichtbarer Dialog hat keinen \
zugänglichen Namen, oder ein aktiver Bereich ist gleichzeitig inert markiert.",
typical_cause_en:
"popovertarget points to a missing element, a visible dialog has no accessible \
name, or an active region is marked inert at the same time.",
recommendation:
"Popover-Ziele prüfen, sichtbare Dialoge und Menüs benennen und inert nur für \
tatsächlich inaktive Bereiche verwenden.",
recommendation_en:
"Check popover targets, name visible dialogs and menus, and use inert only for \
regions that are truly inactive.",
technical_note:
"popovertarget muss auf ein Element mit popover zeigen. Offene Dialoge, Menüs und \
Popover brauchen aria-label, aria-labelledby oder einen geeigneten Namen.",
technical_note_en:
"popovertarget must reference an element with popover. Open dialogs, menus, and \
popovers need aria-label, aria-labelledby, or another suitable name.",
responsible_role: Role::Development,
effort_estimate: Effort::Quick,
example_bad: Some("<button popovertarget=\"menu\">Menu</button>\n<div id=\"menu\">...</div>"),
example_good: Some("<button popovertarget=\"menu\">Menu</button>\n<div id=\"menu\" popover aria-label=\"Menu\">...</div>"),
example_decorative: None,
},
),
(
"seo.headings.long_heading",
RuleExplanation {
customer_title: "Überschrift zu lang",
customer_title_en: "Heading too long",
customer_description:
"Überschriften auf der Seite überschreiten die empfohlene Länge von \
70 Zeichen. Lange Überschriften werden in Suchergebnissen abgeschnitten \
und sind für Leser schwerer zu erfassen.",
customer_description_en:
"Headings on the page exceed the recommended length of 70 characters. \
Long headings are truncated in search results and harder for readers to scan.",
user_impact:
"In Suchergebnissen und sozialen Medien werden die Überschriften \
abgeschnitten, was den Klickanreiz verringert. Leser müssen mehr \
kognitive Arbeit leisten, um den Inhalt einzuordnen.",
user_impact_en:
"In search results and social media the headings are cut off, reducing \
click-through appeal. Readers need more cognitive effort to understand \
the content at a glance.",
typical_cause:
"Redaktionelle Texte werden ungekürzt als Überschrift eingesetzt. \
CMS-Felder für Überschriften haben keine Längenbeschränkung.",
typical_cause_en:
"Editorial text is used as a heading without trimming. \
CMS heading fields have no character limit enforced.",
recommendation:
"Überschriften auf unter 70 Zeichen kürzen und den Kern der Aussage \
voranstellen. Längere Beschreibungen gehören in den Fließtext.",
recommendation_en:
"Shorten headings to under 70 characters and lead with the core message. \
Longer descriptions belong in the body text.",
technical_note:
"Google zeigt typischerweise 50–60 Zeichen im Title-Tag an. \
Für H2/H3 gilt keine feste Grenze, aber über 70 Zeichen sinkt \
die Lesbarkeit deutlich. CMS-Validierung empfohlen.",
technical_note_en:
"Google typically displays 50–60 characters in the title tag. \
There is no hard limit for H2/H3, but readability drops noticeably \
beyond 70 characters. CMS-level validation is recommended.",
responsible_role: Role::Editorial,
effort_estimate: Effort::Quick,
example_bad: None,
example_good: None,
example_decorative: None,
},
),
(
"seo.headings.skipped_level",
RuleExplanation {
customer_title: "Übersprungene Überschriftenebene",
customer_title_en: "Skipped heading level",
customer_description:
"Die Überschriften auf der Seite überspringen eine Ebene — zum Beispiel \
folgt auf eine H1 direkt eine H3, ohne dass eine H2 dazwischen steht. \
Das zerstört die logische Hierarchie, die Suchmaschinen und \
Screenreader zur Navigation nutzen.",
customer_description_en:
"The headings on the page skip a level — for example, an H1 is \
directly followed by an H3 without an H2 in between. This breaks the \
logical hierarchy that search engines and screen readers use for navigation.",
user_impact:
"Screenreader-Nutzer navigieren Seiten über Überschriften. Fehlende \
Ebenen unterbrechen diesen Pfad und erschweren die Orientierung. \
Suchmaschinen bewerten Seiten mit konsistenter Überschriftenstruktur \
als inhaltlich hochwertiger.",
user_impact_en:
"Screen reader users navigate pages via headings. Missing levels \
interrupt this path and make orientation harder. Search engines \
rate pages with consistent heading structure as higher quality content.",
typical_cause:
"CMS-Templates oder Komponenten setzen fest kodierte Überschriftenebenen \
(z. B. immer H3 in Footer-Spalten), unabhängig davon, was der Seitenkontext \
erfordert. Entwickler wählen Ebenen nach visuellem Stil statt nach Hierarchie.",
typical_cause_en:
"CMS templates or components hard-code heading levels (e.g. always H3 \
in footer columns) regardless of page context. Developers choose levels \
for visual style rather than hierarchy.",
recommendation:
"Überschriftenhierarchie von H1 abwärts lückenlos einhalten: H1 → H2 → H3. \
Wenn eine Überschrift visuell kleiner wirken soll, CSS-Klassen verwenden \
statt die HTML-Ebene zu ändern.",
recommendation_en:
"Maintain heading hierarchy from H1 downward without gaps: H1 → H2 → H3. \
To make a heading look smaller visually, use CSS classes rather than \
changing the HTML level.",
technical_note:
"WCAG 1.3.1 (Info and Relationships, Level A) erfordert, dass Struktur \
programmatisch erkennbar ist. Übersprungene Ebenen verletzen diese Anforderung. \
Screenreader wie NVDA/JAWS springen mit H-Tasten — Lücken erzeugen tote Punkte.",
technical_note_en:
"WCAG 1.3.1 (Info and Relationships, Level A) requires structure to be \
programmatically determinable. Skipped levels violate this requirement. \
Screen readers like NVDA/JAWS jump with H-keys — gaps create dead spots.",
responsible_role: Role::Development,
effort_estimate: Effort::Quick,
example_bad: Some("<h1>Page title</h1>\n<h3>Section</h3>"),
example_good: Some("<h1>Page title</h1>\n<h2>Section</h2>"),
example_decorative: None,
},
),
];
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn taxonomy_keyed_rules_have_localized_recommendation() {
for rule_id in [
"a11y.aria_hidden_focus.invalid",
"a11y.aria_prohibited_attr.invalid",
"a11y.modern_attributes.invalid",
] {
let expl = get_explanation(rule_id)
.unwrap_or_else(|| panic!("missing explanation for {rule_id}"));
let de = expl.recommendation_for("de");
let en = expl.recommendation_for("en");
assert!(!de.is_empty(), "{rule_id}: empty DE recommendation");
assert!(!en.is_empty(), "{rule_id}: empty EN recommendation");
assert_ne!(de, en, "{rule_id}: DE and EN recommendation identical");
}
}
#[test]
fn code_examples_are_canonical_english() {
let has_umlaut = |s: &str| s.chars().any(|c| "äöüßÄÖÜ".contains(c));
for (rule_id, expl) in EXPLANATIONS {
for example in [expl.example_bad, expl.example_good, expl.example_decorative]
.into_iter()
.flatten()
{
assert!(
!has_umlaut(example),
"{rule_id}: code example contains German text: {example}"
);
}
}
}
}