Komponente für Designschalter erstellen

Grundlegende Informationen zum Erstellen einer adaptiven und barrierefreien „Design Switch“-Komponente.

In diesem Beitrag möchte ich Ihnen zeigen, wie Sie eine Komponente für den dunklen und hellen Design-Schalter erstellen können. Demo ansehen.

Demo-Schaltfläche für bessere Sichtbarkeit vergrößert

Falls du lieber ein Video hast, findest du hier eine YouTube-Version dieses Beitrags:

Überblick

Eine Website bietet möglicherweise Einstellungen zur Steuerung des Farbschemas, anstatt sich vollständig auf die Systempräferenz zu verlassen. Dies bedeutet, dass Nutzer möglicherweise in einem anderen Modus als ihren Systemeinstellungen surfen. Das System eines Nutzers hat beispielsweise ein helles Design, aber er möchte, dass die Website im dunklen Design angezeigt wird.

Beim Erstellen dieser Funktion sind mehrere Überlegungen zum Web-Engineering zu berücksichtigen. Beispielsweise sollte der Browser so schnell wie möglich über die Einstellung informiert werden, um Farbblitze auf der Seite zu verhindern. Das Steuerelement muss zuerst mit dem System synchronisiert und dann clientseitig gespeicherte Ausnahmen zulassen.

Das Diagramm zeigt eine Vorschau der Ereignisse vom Typ „JavaScript-Seitenaufbau und Dokumentinteraktionen“, um zu zeigen, dass es 4 Möglichkeiten zum Festlegen des Themas gibt

Markup

Für die Ein/Aus-Schaltfläche sollte ein <button> verwendet werden, da Sie dann von den vom Browser bereitgestellten Interaktionsereignissen und ‐funktionen wie Klickereignissen und Fokussierbarkeit profitieren.

Die Taste

Die Schaltfläche benötigt eine Klasse zur Verwendung über CSS und eine ID für die Verwendung in JavaScript. Da es sich bei dem Inhalt der Schaltfläche außerdem um ein Symbol und nicht um Text handelt, fügen Sie außerdem das Attribut title hinzu, um Informationen zur Funktion der Schaltfläche anzugeben. Fügen Sie zuletzt ein [aria-label] hinzu, um den Status der Symbolschaltfläche beizubehalten, damit Screenreader den Status des Designs für sehbehinderte Nutzer 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

Wenn Sie Screenreadern informieren möchten, dass Änderungen an aria-label angekündigt werden sollen, 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 dieses Markup wird Screenreadern angewiesen, dem Nutzer statt über aria-live="assertive" höflich mitzuteilen, was geändert wurde. Bei dieser Schaltfläche wird „Hell“ oder „Dunkel“ angesagt, je nachdem, wie aria-label geworden ist.

Das Symbol für skalierbare Vektorgrafik (SVG)

Mit SVG können Sie hochwertige, skalierbare Formen mit minimalem Markup erstellen. Durch das Interagieren mit der Schaltfläche können neue visuelle Status für die Vektoren ausgelöst werden, wodurch sich SVG hervorragend für Symbole eignet.

Das folgende SVG-Markup wird in <button> eingefügt:

<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
  …
</svg>

aria-hidden wurde dem SVG-Element hinzugefügt, damit Screenreader es ignorieren müssen, wenn es als Präsentation gekennzeichnet ist. Dies ist besonders nützlich für optische Verzierungen wie das Symbol innerhalb einer Schaltfläche. Zusätzlich zum erforderlichen Attribut viewBox für das Element können Sie aus ähnlichen Gründen, warum Bilder Inline-Größen erhalten sollten, auch Höhe und Breite hinzufügen.

Der Sonne

Das Sonnensymbol mit verblassten Sonnenstrahlen und ein roter Pfeil, der auf den Kreis in der Mitte zeigt.

Die Sonnengrafik besteht aus einem Kreis und Linien, für die SVG praktisch Formen hat. <circle> wird zentriert, indem die Attribute cx und cy auf „12“ gesetzt werden. Das entspricht der Hälfte der Größe des Darstellungsbereichs (24) und dem Radius (r) von 6, mit dem die Größe festgelegt wird.

<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>

Darüber hinaus verweist das Attribut „mask“ auf die ID eines SVG-Elements, die Sie als Nächstes erstellen, und erhält schließlich eine Füllfarbe, die der Textfarbe der Seite mit currentColor entspricht.

Die Sonne

Das Sonnensymbol mit blass dargestelltem Sonnenmittel und einem rosafarbenen Pfeil, der auf die Sonnenstrahlen zeigt.

Als Nächstes werden die Sonnenstrahllinien direkt unter dem Kreis in der Gruppe <g> der Gruppe 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 der Strich jeder Linie festgelegt, statt den Wert von fill auf currentColor zu setzen. Die Linien und die Kreisformen erzeugen eine schöne Sonne mit Strahlen.

Mond

Der Mond ist eine Ergänzung des Sonnensymbols mit einer SVG-Maske, um den Anschein eines nahtlosen Übergangs zwischen Licht (Sonne) und Dunkelheit (Mond) zu erzeugen.

<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>
Grafik mit drei vertikalen Ebenen, die zeigt, wie Maskierung funktioniert. Die oberste Schicht ist ein weißes Quadrat mit einem schwarzen Kreis. Die mittlere Ebene ist das Sonnensymbol.
Die untere Ebene ist als Ergebnis beschriftet und zeigt das Sonnensymbol mit einer Aussparung an der Stelle, wo sich der schwarze Kreis der obersten Ebene befindet.

Masken mit SVG sind leistungsstark. Mit den Farben Weiß und Schwarz können Teile einer anderen Grafik entweder entfernt oder eingefügt werden. Das Sonnensymbol wird durch eine Mondform <circle> mit einer SVG-Maske verdeckt. Dazu wird einfach eine Kreisform in einen Maskenbereich verschoben und daraus entfernt.

Was passiert, wenn CSS nicht geladen wird?

Screenshot einer einfachen Browserschaltfläche mit dem Sonnensymbol darin.

Es kann hilfreich sein, Ihr SVG so zu testen, als ob CSS nicht geladen würde, um sicherzustellen, dass das Ergebnis nicht zu groß ist oder Layoutprobleme verursacht. Die Inline-Attribute für Höhe und Breite im SVG sowie die Verwendung von currentColor geben minimale Stilregeln an, die der Browser verwenden muss, wenn CSS nicht geladen wird. Dies ermöglicht einen schönen Schutz vor Netzwerkturbulenzen.

Layout

Die Komponente „Designwechsel“ hat nur wenig Fläche, sodass Sie für das Layout kein Raster oder eine Flexbox 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 für die Übergabe an SVG.

Die erste Aufgabe besteht darin, die Schaltfläche zu einem Kreis zu machen und die Standardschaltflächenstile zu entfernen:

.theme-toggle {
  --size: 2rem;
  
  background: none;
  border: none;
  padding: 0;

  inline-size: var(--size);
  block-size: var(--size);
  aspect-ratio: 1;
  border-radius: 50%;
}

Als Nächstes fügen Sie Interaktionsstile hinzu. Fügen Sie einen Cursorstil für Mausnutzer hinzu. Füge touch-action: manipulation hinzu, um schnell reagierende Berührungen zu ermöglichen. Entfernen Sie die halbtransparente Hervorhebung, die iOS auf Schaltflächen anwendet. Geben Sie dem Fokuszustand zuletzt etwas Luft vom Rand des Elements aus:

.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;
}

Für das SVG innerhalb der Schaltfläche sind ebenfalls einige Stile erforderlich. Die SVG-Datei sollte in die Größe der Schaltfläche passen und die Linienenden für visuelle Weichheit runden:

.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 hover-Medienabfrage

Die Größe der Symbolschaltfläche ist bei 2rem etwas klein. Das ist für Mausnutzer in Ordnung, kann aber bei einem groben Zeiger wie einem Finger schwierig sein. Sorgen Sie dafür, dass die Schaltfläche vielen Richtlinien für die Touch-Größe entspricht, indem Sie eine Hover-Medienabfrage verwenden, um eine Größenerhöhung anzugeben.

.theme-toggle {
  --size: 2rem;
  …
  
  @media (hover: none) {
    --size: 48px;
  }
}

Sonne und Mond SVG-Stile

Die Schaltfläche enthält die interaktiven Aspekte der Komponente „Designwechsel“, während SVG im Inneren die visuellen und animierten Aspekte enthält. Hier kann das Symbol schön gestaltet und zum Leben erweckt werden.

Helles Design

ALT_TEXT_HERE

Wenn Animationen zum Skalieren und Drehen von der Mitte der SVG-Formen aus ausgeführt werden sollen, legen Sie deren transform-origin: center center fest. Die adaptiven Farben der Schaltfläche werden hier bei den Formen verwendet. Mond und Sonne verwenden die Schaltfläche var(--icon-fill) und var(--icon-fill-hover) zur Füllung, während die Sonnenstrahlen die Variablen für den Strich verwenden.

.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

ALT_TEXT_HERE

Die Mondstile müssen die Sonnenstrahlen entfernen, den Sonnenkreis vergrößern und die kreisförmige Maske verschieben.

.sun-and-moon {
  @nest [data-theme="dark"] & {
    & > .sun {
      transform: scale(1.75);
    }

    & > .sun-beams {
      opacity: 0;
    }

    & > .moon > circle {
      transform: translateX(-7px);

      @supports (cx: 1) {
        transform: translateX(0);
        cx: 17;
      }
    }
  }
}

Beachten Sie, dass das dunkle Design keine Farbänderungen oder Übergänge aufweist. Die übergeordnete Schaltflächenkomponente besitzt die Farben, wo sie in einem dunklen und hellen Kontext bereits adaptiv sind. Die Informationen zum Übergang sollten sich hinter der Medienabfrage für Bewegungspräferenzen des Nutzers befinden.

Animation

Die Schaltfläche sollte funktionsfähig und zustandsorientiert, aber ohne Übergänge an dieser Stelle sein. In den folgenden Abschnitten wird beschrieben, wie Sie Wie und Was definieren.

Medienabfragen teilen und Easings importieren

Damit den Bewegungseinstellungen des Betriebssystems eines Nutzers Übergänge und Animationen hinzugefügt werden können, ermöglicht das PostCSS-Plug-in Custom Media die Verwendung der Syntax der Entwurfs-CSS-Spezifikation für Medienabfragevariablen:

@custom-media --motionOK (prefers-reduced-motion: no-preference);

/* usage example */
@media (--motionOK) {
  .sun {
    transition: transform .5s var(--ease-elastic-3);
  }
}

Wenn Sie individuelle und nutzerfreundliche CSS-Easings erhalten möchten, importieren Sie den Abschnitt easings von Open Props:

@import "https://unpkg.com/open-props/easings.min.css";

/* usage example */
.sun {
  transition: transform .5s var(--ease-elastic-3);
}

Der Sonne

Die Sonnenübergänge sind verspielter als der Mond, was durch hüpfende Easings erreicht wird. Die Sonnenstrahlen sollten bei ihrer Drehung eine kleine Menge reflektieren, und das Zentrum der Sonne sollte bei der Skalierung eine kleine Menge reflektieren.

Die Standardstile (helles Design) definieren die Übergänge und die dunklen Designs 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 der Chrome-Entwicklertools finden Sie eine Zeitachse für Animationsübergänge. Die Dauer der gesamten Animation, die Elemente und das Easing-Timing können überprüft werden.

Übergang von hell zu dunkel
Übergang von dunkel zu hell

Mond

Die Positionen für Mondlicht und Dunkel sind bereits festgelegt. Fügen Sie der --motionOK-Medienabfrage Übergangsstile hinzu, um sie lebendiger zu gestalten und gleichzeitig die Bewegungseinstellungen des Nutzers zu berücksichtigen.

Das Timing mit Verzögerung und Dauer ist entscheidend für einen sauberen Übergang. Wird die Sonne beispielsweise zu früh verdunkelt, wirkt der Übergang nicht orchestriert oder spielerisch und wirkt chaotisch.

​​.sun-and-moon {
  @media (--motionOK) {
    & .moon > circle {
      transform: translateX(-7px);
      transition: transform .25s var(--ease-out-5);

      @supports (cx: 1) {
        transform: translateX(0);
        cx: 17;
        transition: cx .25s var(--ease-out-5);
      }
    }

    @nest [data-theme="dark"] & {
      & > .moon > circle {
        transition-delay: .25s;
        transition-duration: .5s;
      }
    }
  }
}
Übergang von hell zu dunkel
Übergang von dunkel zu hell

Bevorzugt eingeschränkte Bewegung

Bei den meisten GUI-Herausforderungen versuche ich, einige Animationen wie Überblendungen der Deckkraft für Nutzer beizubehalten, die weniger Bewegung bevorzugen. Diese Komponente funktioniert bei sofortigen Statusänderungen jedoch besser.

JavaScript

In dieser Komponente gibt es viel Arbeit für JavaScript, vom Verwalten von ARIA-Informationen für Screenreader bis zum Abrufen und Festlegen von Werten aus dem lokalen Speicher.

Der Seitenaufbau

Es war wichtig, dass beim Seitenaufbau keine Farben blinken. Wenn ein Nutzer mit einem dunklen Farbschema angibt, dass er mit dieser Komponente helles Licht bevorzugt, und die Seite dann neu geladen hat, wäre sie zuerst dunkel und leuchtet dann auf. Um dies zu verhindern, musste etwas blockierter JavaScript-Code 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, und zwar vor CSS- oder <body>-Markup. Wenn der Browser ein solches nicht markiertes Skript findet, führt er den Code aus und führt ihn vor dem restlichen HTML-Code aus. Wenn Sie diesen Moment der Blockierung sparsam nutzen, kann das HTML-Attribut festgelegt werden, bevor das Haupt-CSS die Seite darstellt. Dadurch wird ein Blitzlicht oder Farben verhindert.

Das JavaScript prüft zuerst die Einstellung des Nutzers im lokalen Speicher. Wenn im Speicher nichts gefunden wird, prüft das JavaScript die Systemeinstellung:

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()
}

Darauf folgt eine Funktion zum Ändern des Dokuments mit den Einstellungen.

const reflectPreference = () => {
  document.firstElementChild
    .setAttribute('data-theme', theme.value)

  document
    .querySelector('#theme-toggle')
    ?.setAttribute('aria-label', theme.value)
}

Ein wichtiger Hinweis an dieser Stelle ist der Status des HTML-Dokument-Parsings. 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-Tag, auch <html>-Tag genannt. Die Funktion versucht, beide festzulegen, um sie synchron zu halten, aber bei der ersten Ausführung kann nur das HTML-Tag festgelegt werden. Der querySelector findet zuerst nichts und der optionale Verkettungsoperator sorgt dafür, dass keine Syntaxfehler auftreten, wenn er nicht gefunden wird und die Funktion „setAttribute“ aufgerufen wird.

Als Nächstes wird die Funktion reflectPreference() sofort aufgerufen, sodass das Attribut data-theme für das HTML-Dokument festgelegt ist:

reflectPreference()

Die Schaltfläche benötigt weiterhin das Attribut. Warten Sie daher auf das Ereignis zum Laden der Seite. Anschließend 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)
}

Das Wechseln

Wenn auf die Schaltfläche geklickt wird, muss das Design im JavaScript-Arbeitsspeicher und im Dokument ausgetauscht werden. Der aktuelle Themewert muss überprüft und über den neuen Status entschieden werden. Sobald der neue Status festgelegt ist, speichern Sie ihn und aktualisieren das Dokument:

const onClick = () => {
  theme.value = theme.value === 'light'
    ? 'dark'
    : 'light'

  setPreference()
}

Synchronisierung mit dem System

Der einzige bei diesem Designwechsel ist die Synchronisierung mit der Systemeinstellung bei Änderungen. Wenn ein Nutzer seine Systemeinstellung ändert, während eine Seite und diese Komponente sichtbar sind, ändert sich der Designwechsel entsprechend der Präferenz des neuen Nutzers. Das geschieht so, als hätte der Nutzer mit dem Designwechsel zeitgleich mit dem Systemwechsel interagiert.

Verwenden Sie dazu JavaScript und ein matchMedia-Ereignis, das auf Änderungen an einer Medienabfrage wartet:

window
  .matchMedia('(prefers-color-scheme: dark)')
  .addEventListener('change', ({matches:isDark}) => {
    theme.value = isDark ? 'dark' : 'light'
    setPreference()
  })
Wenn Sie die macOS-Systemeinstellung ändern, ändert sich auch der Status des Designwechsels

Fazit

Jetzt weißt du, wie ich es gemacht habe. Wie würdest du es erreichen? 🙂

Diversifizieren wir unsere Ansätze und lernen Sie alle Möglichkeiten kennen, wie wir das Web nutzen können. Erstelle eine Demo und twittere mich über Links, und ich füge sie unten zum Abschnitt über Community-Remixe hinzu.

Community-Remixe