Was macht website-design-system?
Zuständig für die visuelle Schicht (Aussehen und Performance): Mobilgrafik, Dark Mode und die Theme-Tokens. Erzeugt/pflegt BRAND.md (Vorlage: die templates/brand.md des new-website-Skills) und den Token-Block in der Datei templates/astro/src/styles/global.css dieses Skills. Bildregeln sind unten aufgeführt (WebP/AVIF via Astros astro:assets); der Test images.spec.ts setzt sie durch. Verwende das image-Skill zur Erzeugung von Bild-Assets.
Dark Mode (erstklassig)
- Tokens, keine hartcodierten Farben. Alle Farben fließen durch CSS-Custom-Properties; Light ist der
:root-Standard, Dark überschreibt unter:root[data-theme="dark"], plus ein@media (prefers-color-scheme: dark)-Block für Besucher ohne JavaScript. Das Starterpaket bringt genau diese Struktur mit. - No-FOUC-Skript im
<head>setztdata-themeauslocalStorage.themeoder der Systemeinstellung vor dem ersten Rendern (bereits inBase.astroeingebunden). Der Schalter schreibtlocalStorage.theme(das Starterpaket enthält keinen; falls du einen hinzufügst, erwähne dies in Abschnitt 3 der Datenschutzseite). - Kontrast in beiden Modis: Jedes Text/Hintergrund-Paar erfüllt WCAG AA (4,5:1 für Fließtext, 3:1 für große / UI-Elemente). Eine Farbe, die im Light-Modus funktioniert, versagt oft im Dark-Modus – der a11y-Test durchläuft die gesamte Light+Dark-Matrix, also überprüfe beide.
Mobile & responsive Grafiken
- Mobile First: Gestaltung ab 360px; Zielgrößen ≥ 44px; kein horizontaler Scroll. Prüfe bei 360 / 768 / 1280 (preview_resize).
- Moderne Formate: WebP (oder AVIF) statt JPEG/PNG für Fotos.
- Größe passend zum Anzeigebereich, nicht zur Quelle. Versende niemals ein 2000px-Bild in einen 80px-Slot; erzeuge Breitenvarianten und verwende
srcset/sizes(oder<picture>). - Keine Layoutverschiebung: Setze explizite
width/height-Attribute, damit der Kasten reserviert wird, bevor das Bild geladen wird (schützt CLS/LCP). Astros<Image>setzt sie automatisch;images.spec.tserfordert die Attribute (ein reines CSS-aspect-ratio erfüllt den Test nicht). - Lazy vs. Eager:
loading="lazy"unterhalb des Falzes; das LCP/Hero-Bild lädt eager mitfetchpriority="high"(nicht lazy laden). - Alt-Text auf jedem Bild (
alt=""nur bei rein dekorativen Bildern). - Schriften: Selbst gehostete woff2 mit
font-display: swap; vorladen nur die 1–2 Schriften, die oberhalb des Falzes verwendet werden. Vermeide Layoutverschiebung durch späte Schriftwechsel.
Textlängen-Responsivität (übersetzte Texte)
- Übersetzte Texte sind länger. Deutsch (und viele andere Sprachen) ist meist 20–35 % länger als der entsprechende englische Text. Ein Button, Navigationslink oder eine Karte, die auf englischen Platzhaltertext zugeschnitten ist, läuft beim Einsetzen der echten Übersetzung über, bricht unschön um oder wird abgeschnitten.
- Bevorzuge flexible Breiten. Verwende
min-width/ Flex / Gridauto/frfür Elemente mit übersetztem Text, keine festewidth, die auf den englischen String zugeschnitten ist. - Überlauf ≠ Breite: Flexibles Maß verhindert nicht, dass ein einzelnes langes zusammengesetztes Nomen (z. B. „Datenschutzerklärung“) eine schmale Spalte sprengt – dafür braucht es
hyphens: auto(korrekt pro Sprache über daslang-Attribut, das Base.astro bereits setzt) plusoverflow-wrap: break-wordals Fallback; das Starterpaketsglobal.cssliefert beides auf dembody. Tabellenzellen schalten die Silbentrennung aus (siehe Tabellen). - Verlasse dich nicht nur auf
white-space: nowrap+text-overflow: ellipsisfür Navigationslinks oder Button-Label – das Abschneiden eines Nav-Links ist in der Regel keine akzeptable UX (der Besucher kann nicht mehr erkennen, wohin es führt). In Ordnung für Inhalte, bei denen Abschneiden ein akzeptierter Kompromiss ist (z. B. eine Tabellenzelle), nicht für Navigation. - Prüfe mit echten übersetzten Labels, nicht mit englischen Platzhaltern. Wenn eine Site
astro-i18n-setupsLanguageSwitcher.astrooder eine mehrsprachige Navigation verwendet, überprüfe das Layout bei 360/768/1280 (preview_resize) mit dem tatsächlichen Text in jeder Sprache – nicht nur mit dem englischen Platzhalter.
Tabellen
Ein Stil für alle Tabellen; eine Seite enthält kein layout-CSS für eine Tabelle. Er lebt im Starterpaket in src/styles/tables.css (nach global.css importiert von Base.astro) und tests/tables.spec.ts prüft ihn. Eine Seite, die vor diesem Abschnitt erstellt wurde, kopiert tables.css hinein, fügt die Importzeile in ihr Layout ein und entfernt zuerst das eigene tabellen-CSS jeder Seite.
Zwei Arten, unterschieden nach dem Inhalt der Zellen:
stack(Sätze): eine Tabelle oberhalb von 40rem; darunter stapelt sich jede Zeile, der Zeilenkopf als Titel und jede Zelle unter dem Spaltennamen. Vier Spalten mit Prosa passen nicht auf 320px.figures(kurze Zahlen,class="num"auf einer Zahl-Zelle): bleibt auf dem Handy eine Tabelle und muss in 320px passen. Wenn es nicht mehr passt, mach es zustack; eine Bildlaufleiste ist nicht die Lösung.stack figureszusammen ergibt das Zahlen-Look mit dem gestapelten Layout.
Auszeichnung, jede Tabelle:
<div class="table-wrap stack" role="region" aria-label="Was die Tabelle zeigt" tabindex="0">
<table role="table">
<thead role="rowgroup"><tr role="row">
<th scope="col" role="columnheader">{cols[0]}</th><th scope="col" role="columnheader">{cols[1]}</th>
</tr></thead>
<tbody role="rowgroup">{rows.map((r) => (
<tr role="row"><th scope="row" role="rowheader">{r.item}</th><td role="cell" data-label={cols[1]}>{r.text}</td></tr>
))}</tbody>
</table>
</div>
- Schreibe die Spaltennamen einmal in eine Liste und verwende sie für den Header und für jedes
data-label: das Label einer gestapelten Zelle muss dem Spalten-Header entsprechen, und der Test vergleicht beide. Einefigures-Tabelle benötigt keine rollenspezifischen Attribute pro Zelle und keindata-label. - Eine gestapelte Tabelle hat nicht mehr
display: table. Die Rollen sind ein Fallback für Browser, die die Tabelle beim Layoutwechsel fallen lassen; ob sie dort funktionieren, wird nicht geprüft. In Chromium, gemessen an einer echten Tabelle, behält der Accessibility-Baum die Tabelle auch ohne sie. class="tap"bei einem Link, der allein in einer Zelle steht, macht ihn auf dem Handy zu einem 44px-Ziel.nowraphält einen kurzen Wert („1, 3, 6“) in einer Zeile.long-head(mitstack) verbreitert die erste Spalte, wenn die Zeilenköpfe Sätze sind.- Wörter werden in Zellen nicht getrennt. Die
hyphens: autodes Body zerlegt eine schmale erste Spalte in Fragmente („An-thropic“), daher setzt der Block dorthyphens: manual. - Eine Seite, deren Tabellen von außen kommen und die Auszeichnung nicht tragen können, kommt in
TABLE_EXEMPTin der Spezifikation, mit Begründung.PAGESbleibt vollständig: Die Sitemap und diellms.txt-Prüfungen benötigen jede lebende Route darin.
Was tables.spec.ts prüft: Jede Tabelle befindet sich in einem .table-wrap einer Art mit ihrer Region, Label und tabindex, scope="col" auf jeder Header-Zelle und ein <th scope="row"> als erste Zelle jeder Zeile, mit <td> für den Rest; eine gestapelte Tabelle hat ihre Rollen und ein Label, das dem Header auf jeder Zelle entspricht; bei 360 und 320px scrollt weder eine Seite noch eine Tabelle seitwärts; bei 641px und darüber ist eine gestapelte Tabelle wieder eine Tabelle mit ihrer Header-Zeile und kein Wort gebrochen (dies fängt Regeln wie overflow-wrap: anywhere ab, die eine Spalte ein Wort quetschen lassen); keine Zelle hat hyphens: auto. Sie testet auch das CSS an einem Fixture, das sie auf der Startseite platziert, mit aXe in beiden Modi, sodass eine Seite ohne Tabelle dennoch den Block belegt. Headless Chromium trennt nicht, daher liest die Silbentrennungsprüfung den berechneten Style, nicht das Layout. Safari und VoiceOver werden nicht geprüft.
E-Mail-Verschleierung (Adressen vor Crawlern verbergen)
Setze niemals eine Klartext-E-Mail oder mailto:deine@adresse in den HTML-Quelltext – Scraper ernten sie innerhalb von Stunden für Spam. Rendere jede Adresse durch die EmailLink.astro-Komponente (src/components/EmailLink.astro im new-website-Skills templates/astro/, plus ihr lib/obfuscate.ts-Helper): Die Adresse wird zur Build-Zeit umgekehrt + base64-kodiert und im Browser wieder zusammengesetzt, sodass das ausgelieferte HTML keine lesbare Adresse enthält. Besucher ohne JavaScript sehen einen dennoch nutzbaren Hinweis „Name [at] Domain [dot] com“.
<EmailLink address="[email protected]" subject="Projektanfrage" text="Mail an uns" />
Regel: Kein roher mailto:-Link und keine wörtliche @-Adresse im gebauten HTML – website-qa prüft dies. Gleiches Prinzip für jede Telefonnummer, die du nicht geerntet haben möchtest.
Brand-Tokens
Spiegle die Palette von BRAND.md in den Token-Block und halte sie synchron. Definiere: Brand (fest) sowie pro Theme heading/ink/muted/accent/link/line/bg/bg-soft/surface. Das Akzent und die Link-Farbe müssen AA auf dem Hintergrund in beiden Modi erfüllen – daher hat jeder Modus seine eigenen. Das optionale website-brand-kit erledigt das Spiegeln mit einer Prüfung: Seine Aufnahme liest die Tabellen von BRAND.md, hält jeden Textfarbe gegen 4,5:1 auf jedem Hintergrund in beiden Modi und schreibt die Werte in alle drei Farbblöcke von global.css (Light, Dark und den Dark-Block für Besucher ohne JavaScript). Von Hand müssen alle drei Blöcke zusammen geändert werden.
Fertig bedeutet
BRAND.md ausgefüllt und konsistent mit dem Token-Block; Bilder sind WebP, dimensioniert, lazy/eager-korrekt, mit Alt; der a11y-Test (Light+Dark) ist grün; das Layout hält bei 360/768/1280 ohne Verschiebung; jede Tabelle ist stack oder figures und tables.spec.ts ist grün. Performance-Zahlen (FCP/LCP) sind in website-qa verifiziert. Das Layout hält auch bei 360/768/1280 mit echtem (nicht Platzhalter-)Text in jeder ausgelieferten Sprache, nicht nur in der Standard-Locale.