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.

Demo-Schaltfläche wurde vergrößert, damit sie besser sichtbar ist

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 insgesamt 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 für die Verwendung in CSS und eine ID für die Verwendung in JavaScript. Da der Schaltflächeninhalt ein Symbol und kein Text ist, fügen Sie außerdem das Attribut „title“ hinzu, um Informationen zum Zweck der Schaltfläche anzugeben. 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. In diesem Fall wird „hell“ oder „dunkel“ angesagt, 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 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, da es als „Darstellungselement“ 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 wird mit ausgeblendeten Sonnenstrahlen und einem rosa Pfeil angezeigt, der auf den Kreis in der Mitte zeigt.

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

<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 Sonnenstrahlen direkt unter dem Kreis in einem Gruppenelement <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 ist der Wert für fill nicht currentColor, sondern für stroke für jede Linie festgelegt. Die Linien und Kreisformen bilden eine schöne Sonne mit Strahlen.

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 veranschaulichen Die oberste Schicht ist ein weißes Quadrat mit einem schwarzen Kreis. Die mittlere Schicht 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. Dies bietet eine gute Verteidigung gegen Netzwerkturbulenzen.

Layout

Die Komponente für den Themenwechsel hat eine kleine Oberflä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 einige Interaktionsstile hinzu. Fügen Sie einen Cursorstil für Mausnutzer hinzu. 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 Mediaabfrage für den Hover, 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. Für den Mond und die Sonne werden die Schaltflächen var(--icon-fill) und var(--icon-fill-hover) für die Füllung verwendet, während für die Sonnenstrahlen die Variablen für den Strich verwendet werden.

.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 zu diesem Zeitpunkt funktionsfähig und zustandsabhängig sein, aber ohne Übergänge. In den folgenden Abschnitten geht es darum, wie und was sich bewegen soll.

Medienanfragen teilen und Übergänge 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 Zeitleiste 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 spielerisch, 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

Es war wichtig, dass beim Laden der Seite keine Farben blinken. 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 geladen wird. 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 Parsestatus 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 ein <html>-Tag. Die Funktion versucht, beide zu setzen, um sie synchron zu halten. Beim ersten Ausführen 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, sodass das HTML-Dokument das Attribut data-theme hat:

reflectPreference()

Für die Schaltfläche ist das Attribut noch erforderlich. Warten Sie also 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 Systemeinstellungen ändert, während eine Seite und diese Komponente sichtbar sind, ändert sich der Themenschalter entsprechend der neuen Nutzereinstellung, als hätte der Nutzer gleichzeitig mit dem Systemschalter mit dem Themenschalter interagiert.

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

Wie würden Sie das machen?

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.

Remixe der Community