Eine grundlegende Übersicht darüber, wie Sie eine adaptive und zugängliche Komponente zum Wechseln des Designs erstellen.
In diesem Beitrag möchte ich meine Überlegungen zur Entwicklung einer Komponente zum Umschalten zwischen einem dunklen und einem hellen Design vorstellen. Demo ansehen.
Wenn du lieber ein Video ansehen möchtest, findest du hier eine YouTube-Version dieses Beitrags:
Übersicht
Eine Website kann Einstellungen zum Steuern des Farbschemas bereitstellen, anstatt sich vollständig auf die Systemeinstellung zu verlassen. Das bedeutet, dass Nutzer möglicherweise in einem anderen Modus als ihren Systemeinstellungen surfen. Das System eines Nutzers ist beispielsweise im hellen Modus, aber der Nutzer möchte, dass die Website im dunklen Modus angezeigt wird.
Bei der Entwicklung dieser Funktion sind mehrere Web-Engineering-Aspekte zu berücksichtigen. Der Browser sollte beispielsweise so schnell wie möglich über die Einstellung informiert werden, um Farbblitze auf der Seite zu vermeiden. Das Steuerelement muss zuerst mit dem System synchronisiert werden, bevor clientseitig gespeicherte Ausnahmen zugelassen werden.

Markieren & Zeichnen
Für das Ein/Aus-Element sollte ein <button>
verwendet werden, da Sie dann von den vom Browser bereitgestellten Interaktionsereignissen und Funktionen wie Click-Events und Fokusierbarkeit profitieren.
Die Schaltfläche
Die Schaltfläche benötigt eine Klasse für die Verwendung in CSS und eine ID für die Verwendung in JavaScript.
Da der Schaltflächeninhalt außerdem ein Symbol und kein Text ist, fügen Sie ein title-Attribut hinzu, um Informationen zum Zweck der Schaltfläche bereitzustellen. Fügen Sie als Nächstes ein [aria-label]
hinzu, um den Status der Schaltfläche mit dem Symbol zu speichern, damit Screenreader den Status des Designs für Menschen mit Sehbehinderung freigeben können.
<button
class="theme-toggle"
id="theme-toggle"
title="Toggles light & dark"
aria-label="auto"
>
…
</button>
aria-label
und aria-live
höflich
Damit Screenreader Änderungen an aria-label
ankündigen, fügen Sie der Schaltfläche aria-live="polite"
hinzu.
<button
class="theme-toggle"
id="theme-toggle"
title="Toggles light & dark"
aria-label="auto"
aria-live="polite"
>
…
</button>
Durch diese Markup-Ergänzung wird Screenreadern signalisiert, dass sie dem Nutzer auf höfliche Weise mitteilen sollen, was sich geändert hat, anstatt aria-live="assertive"
zu verwenden. Im Fall dieser Schaltfläche wird je nach aria-label
„hell“ oder „dunkel“ angesagt.
Das Symbol für skalierbare Vektorgrafiken (SVG)
Mit SVG lassen sich hochwertige, skalierbare Formen mit minimalem Markup erstellen. Durch Interaktion mit der Schaltfläche können neue visuelle Status für die Vektoren ausgelöst werden. Daher eignet sich SVG hervorragend für Symbole.
Das folgende SVG-Markup gehört in den <button>
-Container:
<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
…
</svg>
Dem SVG-Element wurde aria-hidden
hinzugefügt, damit Screenreader es ignorieren, da es als präsentationsbezogen gekennzeichnet ist. Das ist ideal für visuelle Elemente wie das Symbol in einer Schaltfläche. Fügen Sie dem Element zusätzlich zum erforderlichen viewBox
-Attribut auch Höhe und Breite hinzu. Die Gründe dafür sind ähnlich wie bei Bildern, die Inline-Größen erhalten sollten.
Die Sonne
Die Sonnengrafik besteht aus einem Kreis und Linien, für die SVG praktischerweise Formen bietet. Der <circle>
-Wert wird zentriert, indem die Eigenschaften cx
und cy
auf 12 gesetzt werden. Das ist die Hälfte der Größe des Darstellungsbereichs (24). Anschließend wird ein Radius (r
) von 6
festgelegt, um die Größe zu bestimmen.
<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
<circle class="sun" cx="12" cy="12" r="6" mask="url(#moon-mask)" fill="currentColor" />
</svg>
Außerdem verweist die Maskeneigenschaft auf die ID eines SVG-Elements, das Sie als Nächstes erstellen, und erhält schließlich eine Füllfarbe, die der Textfarbe der Seite mit currentColor
entspricht.
Die Sonnenstrahlen
Als Nächstes werden die Sonnenstrahlenlinien direkt unter dem Kreis innerhalb des Gruppenelements <g>
hinzugefügt.
<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
<circle class="sun" cx="12" cy="12" r="6" mask="url(#moon-mask)" fill="currentColor" />
<g class="sun-beams" stroke="currentColor">
<line x1="12" y1="1" x2="12" y2="3" />
<line x1="12" y1="21" x2="12" y2="23" />
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
<line x1="1" y1="12" x2="3" y2="12" />
<line x1="21" y1="12" x2="23" y2="12" />
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
</g>
</svg>
Dieses Mal wird nicht der Wert von fill auf currentColor
gesetzt, sondern die stroke jeder Zeile. Die Linien und Kreise bilden eine schöne Sonne mit Strahlen.
Der Mond
Um den Eindruck eines nahtlosen Übergangs zwischen Licht (Sonne) und Dunkelheit (Mond) zu erwecken, wird das Sonnensymbol mithilfe einer SVG-Maske um den Mond erweitert.
<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
<circle class="sun" cx="12" cy="12" r="6" mask="url(#moon-mask)" fill="currentColor" />
<g class="sun-beams" stroke="currentColor">
…
</g>
<mask class="moon" id="moon-mask">
<rect x="0" y="0" width="100%" height="100%" fill="white" />
<circle cx="24" cy="10" r="6" fill="black" />
</mask>
</svg>

Masken mit SVG sind leistungsstark. Mit den Farben Weiß und Schwarz können Sie Teile einer anderen Grafik entfernen oder einfügen. Das Sonnensymbol wird durch eine <circle>
-Form mit einer SVG-Maske verdeckt, indem eine Kreisform in einen Maskenbereich hinein- und wieder herausbewegt wird.
Was passiert, wenn das CSS nicht geladen wird?

Es kann sinnvoll sein, Ihr SVG so zu testen, als ob kein CSS geladen wurde, um sicherzustellen, dass das Ergebnis nicht zu groß ist oder Layoutprobleme verursacht. Die Inline-Attribute „height“ und „width“ für das SVG sowie die Verwendung von currentColor
bieten minimale Styleregeln, die der Browser verwenden kann, wenn CSS nicht geladen wird. Das sorgt für eine gute Abwehr gegen Netzwerkstörungen.
Layout
Die Komponente für das Umschalten des Designs hat eine kleine Oberfläche, sodass Sie kein Raster oder Flexbox für das Layout benötigen. Stattdessen werden SVG-Positionierung und CSS-Transformationen verwendet.
Stile
.theme-toggle
Stile
Das <button>
-Element ist der Container für die Symbolformen und ‑stile. Dieser übergeordnete Kontext enthält adaptive Farben und Größen, die an das SVG-Element übergeben werden.
Als Erstes muss die Schaltfläche in einen Kreis umgewandelt und die Standardformatierungen entfernt werden:
.theme-toggle {
--size: 2rem;
background: none;
border: none;
padding: 0;
inline-size: var(--size);
block-size: var(--size);
aspect-ratio: 1;
border-radius: 50%;
}
Fügen Sie als Nächstes einige Interaktionsarten hinzu. Fügen Sie einen Cursorstil für Mausnutzer hinzu. Fügen Sie touch-action: manipulation
für eine schnelle Reaktion bei Berührung hinzu.
Entfernen Sie die halbtransparente Markierung, die iOS auf Schaltflächen anwendet. Geben Sie dem Fokusstatusumriss etwas Abstand zum Rand des Elements:
.theme-toggle {
--size: 2rem;
background: none;
border: none;
padding: 0;
inline-size: var(--size);
block-size: var(--size);
aspect-ratio: 1;
border-radius: 50%;
cursor: pointer;
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
outline-offset: 5px;
}
Das SVG im Button benötigt ebenfalls einige Formatierungen. Das SVG sollte der Größe der Schaltfläche entsprechen und die Linienenden sollten abgerundet sein, um die Darstellung weicher zu machen:
.theme-toggle {
--size: 2rem;
background: none;
border: none;
padding: 0;
inline-size: var(--size);
block-size: var(--size);
aspect-ratio: 1;
border-radius: 50%;
cursor: pointer;
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
outline-offset: 5px;
& > svg {
inline-size: 100%;
block-size: 100%;
stroke-linecap: round;
}
}
Adaptive Größenanpassung mit der Media-Query hover
Die Größe der Symbolschaltfläche ist mit 2rem
etwas klein. Das ist für Nutzer, die eine Maus verwenden, kein Problem, kann aber für einen groben Zeiger wie einen Finger schwierig sein. Die Schaltfläche entspricht vielen Richtlinien zur Berührungsgröße, da mit einer Hover-Media-Anfrage eine Größensteigerung angegeben wird.
.theme-toggle {
--size: 2rem;
…
@media (hover: none) {
--size: 48px;
}
}
SVG-Stile für Sonne und Mond
Die Schaltfläche enthält die interaktiven Aspekte der Komponente zum Umschalten des Designs, während das SVG die visuellen und animierten Aspekte enthält. Hier kann das Symbol schön gestaltet und zum Leben erweckt werden.
Helles Design

Damit Skalierungs- und Rotationsanimationen vom Mittelpunkt von SVG-Formen aus erfolgen, legen Sie deren transform-origin: center center
fest. Die adaptiven Farben der Schaltfläche werden hier von den Formen verwendet. Für die Füllung von Mond und Sonne werden die Schaltflächen var(--icon-fill)
und var(--icon-fill-hover)
verwendet, für die Sonnenstrahlen die Variablen für den Strich.
.sun-and-moon {
& > :is(.moon, .sun, .sun-beams) {
transform-origin: center center;
}
& > :is(.moon, .sun) {
fill: var(--icon-fill);
@nest .theme-toggle:is(:hover, :focus-visible) > & {
fill: var(--icon-fill-hover);
}
}
& > .sun-beams {
stroke: var(--icon-fill);
stroke-width: 2px;
@nest .theme-toggle:is(:hover, :focus-visible) & {
stroke: var(--icon-fill-hover);
}
}
}
Dunkles Design

Bei den Mondstilen müssen die Sonnenstrahlen entfernt, der Sonnenkreis vergrößert und die Kreismaske verschoben werden.
.sun-and-moon {
@nest [data-theme="dark"] & {
& > .sun {
transform: scale(1.75);
}
& > .sun-beams {
opacity: 0;
}
& > .moon > circle {
transform: translateX(-7px);
@supports (cx: 1px) {
transform: translateX(0);
cx: 17px;
}
}
}
}
Das dunkle Design hat keine Farbänderungen oder Übergänge. Die Farben gehören zur übergeordneten Schaltflächenkomponente, in der sie bereits an dunkle und helle Kontexte angepasst sind. Die Übergangsinformationen sollten sich hinter einer Media-Query für die Bewegungspräferenz des Nutzers befinden.
Animation
Die Schaltfläche sollte funktionsfähig und zustandsbehaftet sein, aber an diesem Punkt keine Übergänge aufweisen. In den folgenden Abschnitten geht es darum, wie und was Übergänge definiert werden.
Media-Queries teilen und Easings importieren
Damit Übergänge und Animationen einfacher an die Bewegungseinstellungen des Betriebssystems eines Nutzers angepasst werden können, ermöglicht das PostCSS-Plug-in Custom Media die Verwendung der Syntax für Variablen für Media-Queries in der CSS-Spezifikation:
@custom-media --motionOK (prefers-reduced-motion: no-preference);
/* usage example */
@media (--motionOK) {
.sun {
transition: transform .5s var(--ease-elastic-3);
}
}
Wenn Sie einzigartige und benutzerfreundliche CSS-Easing-Funktionen verwenden möchten, importieren Sie den easings-Teil von Open Props:
@import "https://unpkg.com/open-props/easings.min.css";
/* usage example */
.sun {
transition: transform .5s var(--ease-elastic-3);
}
Die Sonne
Die Übergänge der Sonne sind verspielter als die des Mondes. Dieser Effekt wird durch federnde Easing-Funktionen erzielt. Die Sonnenstrahlen sollten sich beim Drehen leicht bewegen und das Zentrum der Sonne sollte sich beim Skalieren leicht bewegen.
Die Standardstile (helles Design) definieren die Übergänge und die Stile für das dunkle Design definieren Anpassungen für den Übergang zum hellen Design:
.sun-and-moon {
@media (--motionOK) {
& > .sun {
transition: transform .5s var(--ease-elastic-3);
}
& > .sun-beams {
transition:
transform .5s var(--ease-elastic-4),
opacity .5s var(--ease-3)
;
}
@nest [data-theme="dark"] & {
& > .sun {
transform: scale(1.75);
transition-timing-function: var(--ease-3);
transition-duration: .25s;
}
& > .sun-beams {
transform: rotateZ(-25deg);
transition-duration: .15s;
}
}
}
}
Im Bereich Animation in den Chrome-Entwicklertools finden Sie eine Zeitachse für Animationsübergänge. Die Dauer der gesamten Animation, der Elemente und des Easing-Timings kann geprüft werden.


Der Mond
Die Positionen für hell und dunkel sind bereits festgelegt. Fügen Sie Übergangsstile in die Media-Anfrage --motionOK
ein, um die Animation zum Leben zu erwecken und gleichzeitig die Einstellungen des Nutzers für Bewegungen zu berücksichtigen.
Die Verzögerung und Dauer sind entscheidend für einen reibungslosen Übergang. Wenn die Sonne zu früh verdeckt wird, wirkt der Übergang nicht orchestriert oder verspielt, sondern chaotisch.
.sun-and-moon {
@media (--motionOK) {
& .moon > circle {
transform: translateX(-7px);
transition: transform .25s var(--ease-out-5);
@supports (cx: 1px) {
transform: translateX(0);
cx: 17px;
transition: cx .25s var(--ease-out-5);
}
}
@nest [data-theme="dark"] & {
& > .moon > circle {
transition-delay: .25s;
transition-duration: .5s;
}
}
}
}


Bevorzugt reduzierte Bewegung
In den meisten GUI-Herausforderungen versuche ich, einige Animationen beizubehalten, z. B. Überblendungen der Deckkraft, für Nutzer, die weniger Bewegung bevorzugen. Bei dieser Komponente fühlten sich sofortige Zustandsänderungen jedoch besser an.
JavaScript
In dieser Komponente gibt es viel Arbeit für JavaScript, von der Verwaltung von ARIA-Informationen für Screenreader bis hin zum Abrufen und Festlegen von Werten aus dem lokalen Speicher.
Die Nutzerfreundlichkeit beim Laden von Seiten
Es war wichtig, dass beim Laden der Seite kein Farbblinken auftritt. Wenn ein Nutzer mit einem dunklen Farbschema angibt, dass er mit dieser Komponente lieber ein helles Farbschema verwenden möchte, und die Seite neu geladen wird, ist sie zuerst dunkel und wechselt dann zu hell.
Um dies zu verhindern, musste eine kleine Menge blockierenden JavaScript-Codes ausgeführt werden, um das HTML-Attribut data-theme
so früh wie möglich festzulegen.
<script src="./theme-toggle.js"></script>
Dazu wird zuerst ein einfaches <script>
-Tag im Dokument <head>
geladen, bevor CSS- oder <body>
-Markup geladen wird. Wenn der Browser auf ein nicht markiertes Skript wie dieses trifft, führt er den Code aus, bevor er den restlichen HTML-Code ausführt. Wenn Sie diesen Blockiermoment sparsam einsetzen, können Sie das HTML-Attribut festlegen, bevor das Haupt-CSS die Seite rendert. So wird verhindert, dass die Seite kurz aufblitzt oder Farben angezeigt werden.
Das JavaScript prüft zuerst die Einstellungen des Nutzers im lokalen Speicher und greift auf die Systemeinstellungen zurück, wenn im Speicher nichts gefunden wird:
const storageKey = 'theme-preference'
const getColorPreference = () => {
if (localStorage.getItem(storageKey))
return localStorage.getItem(storageKey)
else
return window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light'
}
Als Nächstes wird eine Funktion zum Festlegen der Nutzereinstellung im lokalen Speicher geparst:
const setPreference = () => {
localStorage.setItem(storageKey, theme.value)
reflectPreference()
}
Gefolgt von einer Funktion zum Ändern des Dokuments anhand der Einstellungen.
const reflectPreference = () => {
document.firstElementChild
.setAttribute('data-theme', theme.value)
document
.querySelector('#theme-toggle')
?.setAttribute('aria-label', theme.value)
}
Wichtig ist an dieser Stelle der Parsing-Status des HTML-Dokuments. Der Browser kennt die Schaltfläche „#theme-toggle“ noch nicht, da das <head>
-Tag noch nicht vollständig geparst wurde. Der Browser hat jedoch ein document.firstElementChild
, also das <html>
-Tag. Die Funktion versucht, beide festzulegen, um sie synchron zu halten. Beim ersten Ausführen kann jedoch nur das HTML-Tag festgelegt werden. Die querySelector
findet zuerst nichts und der Operator für optionales Chaining sorgt dafür, dass keine Syntaxfehler auftreten, wenn sie nicht gefunden wird und versucht wird, die Funktion „setAttribute“ aufzurufen.
Als Nächstes wird die Funktion reflectPreference()
sofort aufgerufen, damit das Attribut data-theme
des HTML-Dokuments festgelegt wird:
reflectPreference()
Die Schaltfläche benötigt das Attribut weiterhin. Warten Sie also auf das Ereignis „Seite laden“. Danach können Sie sicher Abfragen, Listener hinzufügen und Attribute festlegen für:
window.onload = () => {
// set on load so screen readers can get the latest value on the button
reflectPreference()
// now this script can find and listen for clicks on the control
document
.querySelector('#theme-toggle')
.addEventListener('click', onClick)
}
Umschalten
Wenn auf die Schaltfläche geklickt wird, muss das Design im JavaScript-Speicher und im Dokument geändert werden. Der aktuelle Themenwert muss geprüft und eine Entscheidung über seinen neuen Status getroffen werden. Speichern Sie den neuen Status und aktualisieren Sie das Dokument:
const onClick = () => {
theme.value = theme.value === 'light'
? 'dark'
: 'light'
setPreference()
}
Mit dem System synchronisieren
Einzigartig bei diesem Designwechsel ist die Synchronisierung mit der Systemeinstellung, wenn sich diese ändert. Wenn ein Nutzer seine Systemeinstellung ändert, während eine Seite und diese Komponente sichtbar sind, wird die Theme-Schaltfläche an die neue Nutzereinstellung angepasst, als hätte der Nutzer gleichzeitig mit der Theme-Schaltfläche und der Systemumschaltung interagiert.
Das lässt sich mit JavaScript und einem matchMedia
>-Ereignis-Listener für Änderungen an einer Media-Query erreichen:
window
.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', ({matches:isDark}) => {
theme.value = isDark ? 'dark' : 'light'
setPreference()
})
Fazit
Jetzt wissen Sie, wie ich es gemacht habe. Wie würden Sie vorgehen? 🙂
Wir möchten unsere Ansätze diversifizieren und alle Möglichkeiten kennenlernen, die das Web bietet. Erstelle eine Demo, schick mir einen Tweet mit den Links und ich füge sie unten im Bereich „Community-Remixe“ hinzu.