Komponente für Designschalter erstellen

Eine grundlegende Übersicht zum Erstellen einer adaptiven und barrierefreien Komponente zum Wechseln des Designs.

In diesem Beitrag möchte ich meine Gedanken zu einer Komponente zum Umschalten zwischen einem dunklen und einem hellen Design teilen. Demo ansehen.

Die Schaltfläche Demo wurde vergrößert, um die Sichtbarkeit zu verbessern

Wenn du lieber ein Video ansiehst, findest du hier eine YouTube-Version dieses Beitrags:

Übersicht

Eine Website kann Einstellungen zur Steuerung des Farbschemas bereitstellen, anstatt sich ausschließlich auf die Systemeinstellungen zu verlassen. Das bedeutet, dass Nutzer möglicherweise in einem anderen Modus als dem ihrer Systemeinstellungen surfen. Angenommen, das System eines Nutzers ist im hellen Design, der Nutzer möchte aber, dass die Website im dunklen Design angezeigt wird.

Beim Erstellen dieser Funktion müssen mehrere Aspekte des Webengineerings berücksichtigt werden. So sollte der Browser beispielsweise so schnell wie möglich über die Einstellung informiert werden, um ein Flackern der Seitenfarbe zu verhindern. Außerdem muss das Steuerelement zuerst mit dem System synchronisiert werden, bevor clientseitig gespeicherte Ausnahmen zulässig sind.

Das Diagramm zeigt eine Vorschau der JavaScript-Ereignisse für das Laden der Seite und die Interaktion mit dem Dokument, um zu veranschaulichen, dass es vier Pfade zum Festlegen des Themas gibt.

Markieren & Zeichnen

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

Die Schaltfläche

Die Schaltfläche benötigt eine Klasse zur Verwendung in CSS und eine ID zur Verwendung in JavaScript. Da es sich beim Inhalt der Schaltfläche nicht um Text, sondern um ein Symbol handelt, sollten Sie außerdem das Attribut title hinzufügen, um Informationen zum Zweck der Schaltfläche bereitzustellen. Fügen Sie abschließend ein [aria-label] hinzu, um den Status der Symbolschaltfläche zu speichern, damit Screenreader den Status des Themas für sehbehinderte Nutzer vorlesen 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 mitteilen möchten, dass Änderungen an aria-label angesagt 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>

Diese Markierung weist Screenreader an, den Nutzern höflich mitzuteilen, was sich geändert hat, anstatt ihnen aria-live="assertive" zu sagen. Bei dieser Schaltfläche wird „Hell“ oder „Dunkel“ angezeigt, je nachdem, was aus aria-label geworden ist.

Das Symbol für skalierbare Vektorgrafiken (SVG)

Mit SVG können Sie hochwertige, skalierbare Formen mit minimalem Markup erstellen. Durch die Interaktion mit der Schaltfläche können neue visuelle Zustände für die Vektoren ausgelöst werden. Das macht SVG ideal für Symbole.

Das folgende SVG-Markup befindet sich in <button>:

<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, da es als „Darstellung“ gekennzeichnet ist. Das eignet sich hervorragend für visuelle Verzierungen, z. B. für das Symbol in einer Schaltfläche. Fügen Sie dem Element zusätzlich zum erforderlichen viewBox-Attribut die Attribute „height“ und „width“ hinzu. Dies hat ähnliche Gründe wie bei Bildern, die Inline-Größen haben sollten.

Die Sonne

Das Sonnensymbol mit ausgeblendeten Sonnenstrahlen und einem knallrosa Pfeil, der auf den Kreis in der Mitte zeigt.

Die Sonnengrafik besteht aus einem Kreis und Linien, für die SVG praktischerweise Formen hat. <circle> wird zentriert, indem die Eigenschaften cx und cy auf 12 gesetzt werden, was die Hälfte der Größe des Darstellungsbereichs (24) entspricht. Anschließend wird ein Radius (r) von 6 festgelegt, durch den 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>

Außerdem verweist die Eigenschaft „mask“ auf die ID eines SVG-Elements, die Sie als Nächstes erstellen. Schließlich wird eine Füllfarbe angegeben, die mit der Textfarbe der Seite mit currentColor übereinstimmt.

Die Sonne scheint

Das Sonnensymbol, bei dem die Mitte der Sonne ausgeblendet ist, und ein knallrosa Pfeil, der auf die Sonnenstrahlen zeigt.

Als Nächstes werden die Sonnenstrahllinien direkt unter dem Kreis in der Gruppe <g> der Gruppenelemente 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 ist der Wert von fill nicht currentColor, sondern der stroke jeder Linie. Die Linien und die Kreisformen bilden eine schöne Sonne mit Balken.

Mond

Um den Eindruck eines nahtlosen Übergangs zwischen hell (Sonne) und dunkel (Mond) zu erzeugen, ist der Mond eine SVG-Maske, die das Sonnensymbol ergänzt.

<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 die Funktionsweise der Maskierung veranschaulicht. Die oberste Schicht ist ein weißes Quadrat mit einem schwarzen Kreis. Die mittlere Ebene ist das Sonnensymbol.
Die untere Ebene ist als Ergebnis gekennzeichnet und zeigt das Sonnensymbol mit einer Aussparung an der Stelle, an der sich der schwarze Kreis der oberen Ebene befindet.

Masken mit SVG sind leistungsstark. Mit den Farben Weiß und Schwarz können Teile einer anderen Grafik entfernt oder eingeschlossen werden. Das Sonnensymbol wird von einem Mond<circle> mit einer SVG-Maske verdeckt. Dazu müssen Sie lediglich einen Kreis in einen Maskenbereich verschieben und wieder herausbewegen.

Was passiert, wenn CSS nicht geladen wird?

Screenshot einer einfachen Browserschaltfläche mit dem Sonnensymbol

Es kann hilfreich sein, Ihr SVG so zu testen, als würde das CSS nicht geladen, um sicherzustellen, dass das Ergebnis nicht zu groß ist oder Layoutprobleme verursacht. Die Inline-Attribute „height“ und „width“ im SVG sowie die Verwendung von currentColor geben dem Browser minimale Stilregeln, die er verwenden kann, wenn CSS nicht geladen wird. Das bietet eine gute Verteidigung gegen Netzwerkturbulenzen.

Layout

Die Komponente für den Themenwechsel hat nur eine kleine Fläche, sodass Sie für das Layout kein Raster oder 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, die an das SVG übergeben werden.

Zuerst machen wir die Schaltfläche zu einem Kreis und entfernen die Standardstilvorlagen für Schaltflächen:

.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 Interaktionsstile hinzu. Ein Cursorstil für Mausnutzer hinzufügen. Fügen Sie touch-action: manipulation hinzu, um schnell reagierende Touchbedienung zu ermöglichen. Entfernen Sie die halbtransparente Markierung, die iOS auf Schaltflächen anwendet. Lassen Sie zum Schluss etwas Abstand zwischen dem Umriss des Fokusstatus und dem 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;
}

Auch das SVG in der Schaltfläche benötigt einige Stile. Das SVG sollte der Größe der Schaltfläche entsprechen und die Enden der Linien sollten abgerundet sein, um ein weiches Erscheinungsbild zu erzielen:

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

Die Größe der Symbolschaltfläche ist mit 2rem etwas klein. Das ist für Nutzer mit Maus in Ordnung, kann aber für einen groben Zeiger wie einen Finger schwierig sein. Sie können die Schaltfläche so gestalten, dass sie vielen Richtlinien für die Größe von Touchbedienungselementen entspricht. Verwenden Sie dazu eine Hover-Medienabfrage, um eine Größenänderung anzugeben.

.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 „Designschalter“, 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

ALT_TEXT_HERE

Wenn Skalierungs- und Drehanimationen von der Mitte der SVG-Formen erfolgen sollen, legen Sie deren transform-origin: center center fest. Die adaptiven Farben der Schaltfläche werden hier von den Formen verwendet. Mond und Sonne verwenden die Schaltflächen var(--icon-fill) und var(--icon-fill-hover) für ihre 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

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 enthält keine Farbänderungen oder Übergänge. Die übergeordnete Schaltflächenkomponente ist für die Farben verantwortlich, die bereits in einem dunklen und hellen Kontext adaptiv sind. Die Informationen zum Übergang sollten sich hinter der Medienabfrage der Bewegungspräferenz eines Nutzers befinden.

Animation

Die Schaltfläche sollte funktional und zustandsorientiert sein, an dieser Stelle jedoch keine Übergänge haben. In den folgenden Abschnitten geht es darum, wie und was sich über die Übergänge hinweg ändern soll.

Medienabfragen teilen und Easings importieren

Damit Übergänge und Animationen an die Bewegungseinstellungen des Betriebssystems eines Nutzers angepasst werden können, ermöglicht das PostCSS-Plug-in Custom Media die Verwendung der Syntax der vorgeschlagenen CSS-Spezifikation für Variablen für Medienabfragen:

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

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

Wenn Sie einzigartige und einfach zu verwendende CSS-Übergänge benötigen, importieren Sie den Bereich easings 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 Übergänge erreicht. Die Sonnenstrahlen sollten sich beim Drehen leicht bewegen und die Mitte 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 Wechsel 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, die Elemente und das Timing der Übergänge können geprüft werden.

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

Mond

Die Positionen für den hellen und dunklen Mond sind bereits festgelegt. Fügen Sie in der --motionOK-Medienabfrage Übergangsstile hinzu, um den Mond zum Leben zu erwecken und dabei die Bewegungseinstellungen des Nutzers zu berücksichtigen.

Timing, Verzögerung und Dauer sind entscheidend, damit dieser Übergang reibungslos funktioniert. Wenn die Sonne beispielsweise zu früh verdeckt wird, wirkt der Übergang nicht geplant 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;
      }
    }
  }
}
Übergang von hell zu dunkel
Übergang von dunkel zu hell

Weniger Bewegung bevorzugt

Bei den meisten GUI-Herausforderungen versuche ich, für Nutzer, die weniger Bewegung bevorzugen, einige Animationen wie Opacity-Cross-Fades beizubehalten. Diese Komponente eignet sich jedoch besser für sofortige Statusänderungen.

JavaScript

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

Seitenladezeit

Beim Seitenaufbau darf kein Farbblinken auftreten. Wenn ein Nutzer mit einem dunklen Farbschema angibt, dass er für diese Komponente ein helles Farbschema bevorzugt, und dann die Seite neu lädt, ist die Seite zuerst dunkel und dann blinkt sie hell. Um dies zu verhindern, wurde ein kleines JavaScript-Blockierungsskript ausgeführt, 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 verwendet werden. Wenn der Browser auf ein solches nicht gekennzeichnetes Script stößt, wird der Code vor dem Rest des HTML-Codes ausgeführt. Wenn Sie diesen Blockierungsmoment sparsam verwenden, können Sie das HTML-Attribut festlegen, bevor das Haupt-CSS die Seite anzeigt, und so ein Flackern oder Farbänderungen verhindern.

Das JavaScript sucht zuerst im lokalen Speicher nach den Einstellungen des Nutzers und prüft dann die Systemeinstellungen, falls 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 Nutzereinstellungen im lokalen Speicher analysiert:

const setPreference = () => {
  localStorage.setItem(storageKey, theme.value)
  reflectPreference()
}

gefolgt von einer Funktion, um das Dokument mit den Einstellungen zu ändern.

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. Im Browser gibt es jedoch ein document.firstElementChild-Tag, auch <html>-Tag genannt. Die Funktion versucht, beide Parameter so zu konfigurieren, dass sie synchron bleiben. Bei der ersten Ausführung kann jedoch nur das HTML-Tag festgelegt werden. Der querySelector findet zuerst nichts und der optionale Verknüpfungsoperator sorgt dafür, dass keine Syntaxfehler auftreten, wenn er nicht gefunden wird und die setAttribute-Funktion aufgerufen wird.

Als Nächstes wird die Funktion reflectPreference() sofort aufgerufen, damit das Attribut data-theme im HTML-Dokument festgelegt wird:

reflectPreference()

Für die Schaltfläche ist das Attribut noch erforderlich. Warten Sie daher auf das Seitenladeereignis. Danach können Sie die Abfrage ausführen, Listener hinzufügen und Attribute festlegen:

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

Ein-/Ausschalten

Wenn auf die Schaltfläche geklickt wird, muss das Design im JavaScript-Speicher und im Dokument ausgetauscht 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()
}

Synchronisierung mit dem System

Einzigartig bei diesem Designwechsel ist die Synchronisierung mit der sich ändernden Systemeinstellung. Wenn ein Nutzer seine Systemeinstellung ändert, während eine Seite und diese Komponente sichtbar sind, ändert sich der Designwechsel entsprechend der neuen Nutzereinstellung, als ob der Nutzer mit dem Designwechsel gleichzeitig mit dem Systemwechsel interagiert hätte.

Das ist mit JavaScript und einem matchMedia-Ereignis möglich, das auf Änderungen an einer Mediaabfrage wartet:

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

Fazit

Jetzt, wo du weißt, wie ich das gemacht habe, wie würdest du... ‽ 🙂

Lassen Sie uns unsere Ansätze diversifizieren und alle Möglichkeiten kennenlernen, wie Sie im Web entwickeln können. Erstelle eine Demo, tweete mir Links und ich füge sie unten in den Abschnitt „Community-Remixe“ hinzu.

Community-Remixe