Komponente für geteilte Schaltfläche erstellen

Eine grundlegende Übersicht zum Erstellen einer barrierefreien geteilten Schaltfläche.

In diesem Beitrag möchte ich meine Gedanken zu einer Möglichkeit zum Erstellen einer geteilten Schaltfläche teilen . Demo ansehen.

Demo

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

Übersicht

Getrennte Schaltflächen sind Schaltflächen, die eine primäre Schaltfläche und eine Liste zusätzlicher Schaltflächen verbergen. Sie eignen sich gut, um eine häufig verwendete Aktion zu präsentieren und sekundäre, seltener verwendete Aktionen bis zur Verwendung zu verschachteln. Eine geteilte Schaltfläche kann entscheidend dazu beitragen, dass ein überladenes Design minimalistisch wirkt. Eine erweiterte Split-Schaltfläche kann sogar die letzte Nutzeraktion speichern und diese in die primäre Position verschieben.

Eine gängige Schaltfläche zum Splitten finden Sie in Ihrer E-Mail-Anwendung. Die primäre Aktion ist „Senden“. Sie können die Nachricht aber auch später senden oder als Entwurf speichern:

Beispiel für eine geteilte Schaltfläche in einer E-Mail-Anwendung

Der gemeinsame Aktionsbereich ist praktisch, da sich der Nutzer nicht umsehen muss. Er weiß, dass wichtige E-Mail-Aktionen in der geteilten Schaltfläche enthalten sind.

Teile

Sehen wir uns die wichtigsten Teile einer Split-Schaltfläche an, bevor wir die Gesamtkoordination und die endgültige Nutzererfahrung besprechen. Hier wird das Tool zur Prüfung der Barrierefreiheit von VisBug verwendet, um eine Makroansicht der Komponente zu zeigen, in der Aspekte des HTML-Codes, des Stils und der Barrierefreiheit für jeden Hauptteil angezeigt werden.

Die HTML-Elemente, aus denen die geteilte Schaltfläche besteht.

Container für Schaltfläche zum Aufteilen auf oberster Ebene

Die Komponente der obersten Ebene ist ein Inline-Flexbox mit der Klasse gui-split-button, die die primäre Aktion und .gui-popup-button enthält.

Die Klasse „gui-split-button“ wird geprüft und die in dieser Klasse verwendeten CSS-Properties werden angezeigt.

Die primäre Aktionsschaltfläche

Das anfangs sichtbare und fokussierbare <button> passt in den Container. Die beiden übereinstimmenden Eckformen für Fokus, Bewegung des Mauszeigers und aktive Interaktionen sollen innerhalb von .gui-split-button erscheinen.

Der Inspector mit den CSS-Regeln für das Schaltflächenelement

Schaltfläche zum Ein-/Ausschalten des Pop-ups

Das Supportelement „Pop-up-Schaltfläche“ dient zum Aktivieren und Verweis auf die Liste der sekundären Schaltflächen. Beachten Sie, dass es sich nicht um ein <button> handelt und dass es nicht fokussiert werden kann. Es ist jedoch der Positionierungsanker für .gui-popup und der Host für :focus-within, mit dem das Pop-up angezeigt wird.

Der Inspector mit den CSS-Regeln für die Klasse „gui-popup-button“

Pop-up-Karte

Dies ist eine schwebende Karte, die dem Anker .gui-popup-button untergeordnet ist, absolut positioniert ist und die Schaltflächenliste semantisch umbricht.

Der Inspector mit den CSS-Regeln für die Klasse „gui-popup“

Die sekundären Aktionen

Eine fokussierbare <button> mit einer etwas kleineren Schriftgröße als die primäre Aktionsschaltfläche hat ein Symbol und einen Stil, der dem der primären Schaltfläche entspricht.

Der Inspector mit den CSS-Regeln für das Schaltflächenelement

Benutzerdefinierte Eigenschaften

Mit den folgenden Variablen können Sie eine Farbharmonie schaffen und an einem zentralen Ort Werte ändern, die in der gesamten Komponente verwendet werden.

@custom-media --motionOK (prefers-reduced-motion: no-preference);
@custom-media --dark (prefers-color-scheme: dark);
@custom-media --light (prefers-color-scheme: light);

.gui-split-button {
  --theme:             hsl(220 75% 50%);
  --theme-hover:  hsl(220 75% 45%);
  --theme-active:  hsl(220 75% 40%);
  --theme-text:      hsl(220 75% 25%);
  --theme-border: hsl(220 50% 75%);
  --ontheme:         hsl(220 90% 98%);
  --popupbg:         hsl(220 0% 100%);

  --border: 1px solid var(--theme-border);
  --radius: 6px;
  --in-speed: 50ms;
  --out-speed: 300ms;

  @media (--dark) {
    --theme:             hsl(220 50% 60%);
    --theme-hover:  hsl(220 50% 65%);
    --theme-active:  hsl(220 75% 70%);
    --theme-text:      hsl(220 10% 85%);
    --theme-border: hsl(220 20% 70%);
    --ontheme:         hsl(220 90% 5%);
    --popupbg:         hsl(220 10% 30%);
  }
}

Layouts und Farben

Markieren & Zeichnen

Das Element beginnt als <div> mit einem benutzerdefinierten Klassennamen.

<div class="gui-split-button"></div>

Fügen Sie die primäre Schaltfläche und die .gui-popup-button-Elemente hinzu.

<div class="gui-split-button">
  <button>Send</button>
  <span class="gui-popup-button" aria-haspopup="true" aria-expanded="false" title="Open for more actions"></span>
</div>

Beachten Sie die Aria-Attribute aria-haspopup und aria-expanded. Diese Hinweise sind für Screenreader entscheidend, damit sie die Funktion und den Status der geteilten Schaltfläche kennen. Das title-Attribut ist für alle hilfreich.

Fügen Sie ein <svg>-Symbol und das Containerelement .gui-popup hinzu.

<div class="gui-split-button">
  <button>Send</button>
  <span class="gui-popup-button" aria-haspopup="true" aria-expanded="false" title="Open for more actions">
    <svg aria-hidden="true" viewBox="0 0 20 20">
      <path d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" />
    </svg>
    <ul class="gui-popup"></ul>
  </span>
</div>

Bei einem einfachen Pop-up-Placement ist .gui-popup ein untergeordnetes Element der Schaltfläche, mit der es maximiert wird. Der einzige Nachteil dieser Strategie ist, dass der .gui-split-button-Container overflow: hidden nicht verwenden kann, da das Pop-up dadurch nicht sichtbar ist.

Ein <ul> mit <li><button>-Inhalten wird von Screenreadern als „Schaltflächenliste“ angesagt, was genau der angezeigten Benutzeroberfläche entspricht.

<div class="gui-split-button">
  <button>Send</button>
  <span class="gui-popup-button" aria-haspopup="true" aria-expanded="false" title="Open for more actions">
    <svg aria-hidden="true" viewBox="0 0 20 20">
      <path d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" />
    </svg>
    <ul class="gui-popup">
      <li>
        <button>Schedule for later</button>
      </li>
      <li>
        <button>Delete</button>
      </li>
      <li>
        <button>Save draft</button>
      </li>
    </ul>
  </span>
</div>

Für mehr Flair und Farbe habe ich den sekundären Schaltflächen Symbole von https://heroicons.com hinzugefügt. Symbole sind sowohl für die primären als auch für die sekundären Schaltflächen optional.

<div class="gui-split-button">
  <button>Send</button>
  <span class="gui-popup-button" aria-haspopup="true" aria-expanded="false" title="Open for more actions">
    <svg aria-hidden="true" viewBox="0 0 20 20">
      <path d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" />
    </svg>
    <ul class="gui-popup">
      <li><button>
        <svg aria-hidden="true" viewBox="0 0 24 24">
          <path d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
        </svg>
        Schedule for later
      </button></li>
      <li><button>
        <svg aria-hidden="true" viewBox="0 0 24 24">
          <path d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
        </svg>
        Delete
      </button></li>
      <li><button>
        <svg aria-hidden="true" viewBox="0 0 24 24">
          <path d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" />
        </svg>
        Save draft
      </button></li>
    </ul>
  </span>
</div>

Stile

Nachdem Sie HTML und Inhalte hinzugefügt haben, können Sie mithilfe von Stilen Farbe und Layout festlegen.

Container für die geteilte Schaltfläche stylen

Ein inline-flex-Displaytyp eignet sich gut für diese Umbruchkomponente, da sie in anderen geteilten Schaltflächen, Aktionen oder Elementen inline angezeigt werden sollte.

.gui-split-button {
  display: inline-flex;
  border-radius: var(--radius);
  background: var(--theme);
  color: var(--ontheme);
  fill: var(--ontheme);

  touch-action: manipulation;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
}

Die Schaltfläche „Aufteilen“.

Das <button>-Styling

Schaltflächen verschleiern sehr gut, wie viel Code erforderlich ist. Möglicherweise müssen Sie die Browserstandardstile rückgängig machen oder ersetzen. Außerdem müssen Sie eine gewisse Vererbung erzwingen, Interaktionsstatus hinzufügen und sich an verschiedene Nutzereinstellungen und Eingabetypen anpassen. Schaltflächenstilen summieren sich schnell.

Diese Schaltflächen unterscheiden sich von normalen Schaltflächen, da sie denselben Hintergrund wie ein übergeordnetes Element haben. Normalerweise hat eine Schaltfläche einen eigenen Hintergrund und eine eigene Textfarbe. Diese teilen sie jedoch und wenden nur ihren eigenen Hintergrund auf die Interaktion an.

.gui-split-button button {
  cursor: pointer;
  appearance: none;
  background: none;
  border: none;

  display: inline-flex;
  align-items: center;
  gap: 1ch;
  white-space: nowrap;

  font-family: inherit;
  font-size: inherit;
  font-weight: 500;

  padding-block: 1.25ch;
  padding-inline: 2.5ch;

  color: var(--ontheme);
  outline-color: var(--theme);
  outline-offset: -5px;
}

Fügen Sie Interaktionsstatus mit einigen CSS-Pseudoklassen hinzu und verwenden Sie passende benutzerdefinierte Eigenschaften für den Status:

.gui-split-button button {
  

  &:is(:hover, :focus-visible) {
    background: var(--theme-hover);
    color: var(--ontheme);

    & > svg {
      stroke: currentColor;
      fill: none;
    }
  }

  &:active {
    background: var(--theme-active);
  }
}

Die primäre Schaltfläche benötigt einige spezielle Stile, um den Designeffekt zu vervollständigen:

.gui-split-button > button {
  border-end-start-radius: var(--radius);
  border-start-start-radius: var(--radius);

  & > svg {
    fill: none;
    stroke: var(--ontheme);
  }
}

Zum Schluss erhalten die Schaltfläche und das Symbol für das helle Design einen Schatten:

.gui-split-button {
  @media (--light) {
    & > button,
    & button:is(:focus-visible, :hover) {
      text-shadow: 0 1px 0 var(--theme-active);
    }
    & > .gui-popup-button > svg,
    & button:is(:focus-visible, :hover) > svg {
      filter: drop-shadow(0 1px 0 var(--theme-active));
    }
  }
}

Bei einer guten Schaltfläche wurde auf die Mikrointeraktionen und kleinen Details geachtet.

Hinweis zu :focus-visible

Beachten Sie, dass in den Schaltflächenstilen :focus-visible anstelle von :focus verwendet wird. :focus ist ein wichtiger Schritt zur Schaffung einer barrierefreien Benutzeroberfläche, hat aber einen Nachteil: Es wird nicht intelligent ermittelt, ob der Nutzer sie sehen muss oder nicht. Sie wird für jeden Fokus angewendet.

Im Video unten wird versucht, diese Mikrointeraktion zu zerlegen, um zu zeigen, wie :focus-visible eine intelligente Alternative ist.

Pop-up-Schaltfläche stylen

Ein 4ch-Flexbox zum Zentrieren eines Symbols und zum Ankern einer Liste mit Pop-up-Schaltflächen. Wie die primäre Schaltfläche ist sie transparent, bis der Mauszeiger darauf bewegt wird oder eine Interaktion damit erfolgt. Sie wird dann auf die gesamte Fläche gedehnt.

Der Pfeilteil der geteilten Schaltfläche, mit dem das Pop-up ausgelöst wird.

.gui-popup-button {
  inline-size: 4ch;
  cursor: pointer;
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-inline-start: var(--border);
  border-start-end-radius: var(--radius);
  border-end-end-radius: var(--radius);
}

Mit CSS-Verschachtelung und dem funktionalen Selektor :is() können Sie den Hover-, Fokus- und Aktivstatus überlagern:

.gui-popup-button {
  

  &:is(:hover,:focus-within) {
    background: var(--theme-hover);
  }

  /* fixes iOS trying to be helpful */
  &:focus {
    outline: none;
  }

  &:active {
    background: var(--theme-active);
  }
}

Diese Stile sind der Hauptansatzpunkt für das Ein- und Ausblenden des Pop-ups. Wenn das .gui-popup-button eines seiner untergeordneten Elemente focus hat, legen Sie für das Symbol und das Pop-up opacity, die Position und pointer-events fest.

.gui-popup-button {
  

  &:focus-within {
    & > svg {
      transition-duration: var(--in-speed);
      transform: rotateZ(.5turn);
    }
    & > .gui-popup {
      transition-duration: var(--in-speed);
      opacity: 1;
      transform: translateY(0);
      pointer-events: auto;
    }
  }
}

Nachdem Sie die Ein- und Ausblendungsstile fertiggestellt haben, müssen Sie die Übergangstransformationen bedingt je nach Bewegungseinstellung des Nutzers festlegen:

.gui-popup-button {
  

  @media (--motionOK) {
    & > svg {
      transition: transform var(--out-speed) ease;
    }
    & > .gui-popup {
      transform: translateY(5px);

      transition:
        opacity var(--out-speed) ease,
        transform var(--out-speed) ease;
    }
  }
}

Ein aufmerksamer Blick auf den Code zeigt, dass die Deckkraft weiterhin übergeht, wenn Nutzer weniger Bewegung bevorzugen.

Pop-up-Design

Das .gui-popup-Element ist eine schwebende Kartenschaltflächenliste, die mit benutzerdefinierten Eigenschaften und relativen Einheiten so gestaltet ist, dass sie etwas kleiner ist, interaktiv mit der primären Schaltfläche abgeglichen wird und farblich zur Marke passt. Die Symbole haben einen geringeren Kontrast, sind dünner und der Schatten hat einen Hauch der Markenfarbe Blau. Wie bei Schaltflächen sind eine gute Benutzeroberfläche und eine gute Nutzererfahrung das Ergebnis vieler kleiner Details.

Ein schwebendes Kartenelement.

.gui-popup {
  --shadow: 220 70% 15%;
  --shadow-strength: 1%;

  opacity: 0;
  pointer-events: none;

  position: absolute;
  bottom: 80%;
  left: -1.5ch;

  list-style-type: none;
  background: var(--popupbg);
  color: var(--theme-text);
  padding-inline: 0;
  padding-block: .5ch;
  border-radius: var(--radius);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  font-size: .9em;
  transition: opacity var(--out-speed) ease;

  box-shadow:
    0 -2px 5px 0 hsl(var(--shadow) / calc(var(--shadow-strength) + 5%)),
    0 1px 1px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 10%)),
    0 2px 2px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 12%)),
    0 5px 5px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 13%)),
    0 9px 9px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 14%)),
    0 16px 16px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 20%))
  ;
}

Die Symbole und Schaltflächen erhalten Markenfarben, damit sie gut zu den jeweiligen Karten mit dunklem und hellem Design passen:

Links und Symbole für den Bezahlvorgang, die Direktzahlung und „Später kaufen“

.gui-popup {
  

  & svg {
    fill: var(--popupbg);
    stroke: var(--theme);

    @media (prefers-color-scheme: dark) {
      stroke: var(--theme-border);
    }
  }

  & button {
    color: var(--theme-text);
    width: 100%;
  }
}

Das Pop-up im dunklen Design hat zusätzliche Schatten für Text und Symbole sowie einen etwas intensiveren Kastenschatten:

Das Pop-up im dunklen Design.

.gui-popup {
  

  @media (--dark) {
    --shadow-strength: 5%;
    --shadow: 220 3% 2%;

    & button:not(:focus-visible, :hover) {
      text-shadow: 0 1px 0 var(--ontheme);
    }

    & button:not(:focus-visible, :hover) > svg {
      filter: drop-shadow(0 1px 0 var(--ontheme));
    }
  }
}

Allgemeine <svg>-Symbolstile

Alle Symbole werden relativ zur Schaltfläche font-size, in der sie verwendet werden, skaliert. Dabei wird die Einheit ch als inline-size verwendet. Jedem Stil werden auch einige Stile zugewiesen, um die Symbole weich und glatt zu umreißen.

.gui-split-button svg {
  inline-size: 2ch;
  box-sizing: content-box;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-width: 2px;
}

Linksläufiges Layout

Die gesamte komplexe Arbeit wird von logischen Properties erledigt. Hier ist eine Liste der verwendeten logischen Eigenschaften: – display: inline-flex erstellt ein inline-Flex-Element. – padding-block und padding-inline als Paar anstelle der Kurzschreibweise padding bietet die Vorteile der Ausbuchtung der logischen Seiten. – border-end-start-radius und Freunde runden die Ecken je nach Dokumentausrichtung ab. – inline-size statt width sorgt dafür, dass die Größe nicht an physische Abmessungen gebunden ist. – border-inline-start fügt dem Anfang einen Rahmen hinzu, der je nach Skriptrichtung rechts oder links sein kann.

JavaScript

Fast das gesamte folgende JavaScript dient der Barrierefreiheit. Zwei meiner Hilfsbibliotheken werden verwendet, um die Aufgaben ein wenig zu vereinfachen. BlingBlingJS wird für kompakte DOM-Abfragen und die einfache Einrichtung von Ereignis-Listenern verwendet, während roving-ux barrierefreie Tastatur- und Gamepad-Interaktionen für das Pop-up ermöglicht.

import $ from 'blingblingjs'
import {rovingIndex} from 'roving-ux'

const splitButtons = $('.gui-split-button')
const popupButtons = $('.gui-popup-button')

Nachdem die oben genannten Bibliotheken importiert und die Elemente ausgewählt und in Variablen gespeichert wurden, sind nur noch wenige Funktionen erforderlich, um die Website zu aktualisieren.

Roving-Index

Wenn eine Tastatur oder ein Screenreader den Fokus auf die .gui-popup-button legt, soll der Fokus auf die erste (oder zuletzt fokussierte) Schaltfläche in der .gui-popup springen. Die Bibliothek unterstützt uns dabei mit den Parametern element und target.

popupButtons.forEach(element =>
  rovingIndex({
    element,
    target: 'button',
  }))

Das Element übergibt den Fokus jetzt an die Ziel-<button>-Unterelemente und ermöglicht die Standardnavigation mit den Pfeiltasten, um die Optionen zu durchsuchen.

aria-expanded ein-/ausschalten

Es ist zwar visuell erkennbar, dass ein Pop-up angezeigt und ausgeblendet wird, aber ein Screenreader benötigt mehr als visuelle Hinweise. Hier wird JavaScript verwendet, um die CSS-gestützte :focus-within-Interaktion zu ergänzen, indem ein Attribut für Screenreader aktiviert wird.

popupButtons.on('focusin', e => {
  e.currentTarget.setAttribute('aria-expanded', true)
})

popupButtons.on('focusout', e => {
  e.currentTarget.setAttribute('aria-expanded', false)
})

Escape-Schlüssel aktivieren

Der Fokus des Nutzers wurde absichtlich auf eine Falle gelenkt. Daher müssen wir eine Möglichkeit zum Verlassen bereitstellen. Am häufigsten wird die Verwendung des Escape-Schlüssels zugelassen. Achten Sie dazu auf Tastendrücke auf der Pop-up-Schaltfläche, da alle Tastaturereignisse auf untergeordneten Elementen an dieses übergeordnete Element weitergegeben werden.

popupButtons.on('keyup', e => {
  if (e.code === 'Escape')
    e.target.blur()
})

Wenn die Pop-up-Schaltfläche eine Tastenpresse von Escape erkennt, entfernt sie den Fokus mit blur().

Klicks auf Schaltflächen

Wenn der Nutzer auf die Schaltflächen klickt, sie antippt oder die Tastatur verwendet, muss die Anwendung die entsprechende Aktion ausführen. Hier wird wieder die Ereignisweitergabe verwendet, diesmal aber für den .gui-split-button-Container, um Klicks auf Schaltflächen aus einem untergeordneten Pop-up oder der primären Aktion zu erfassen.

splitButtons.on('click', event => {
  if (event.target.nodeName !== 'BUTTON') return
  console.info(event.target.innerText)
})

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