Komponente für geteilte Schaltfläche erstellen

Eine grundlegende Übersicht darüber, wie Sie eine zugängliche Komponente mit geteilter Schaltfläche erstellen.

In diesem Beitrag möchte ich meine Überlegungen zum Erstellen einer geteilten Schaltfläche teilen . Demo ansehen.

Demo

Wenn du lieber ein Video ansehen möchtest, findest du hier eine YouTube-Version dieses Beitrags:

Übersicht

Aufgeteilte Schaltflächen sind Schaltflächen, die eine primäre Schaltfläche und eine Liste mit zusätzlichen Schaltflächen verbergen. Sie sind nützlich, um eine häufige Aktion zu präsentieren und gleichzeitig sekundäre, seltener verwendete Aktionen zu verschachteln, bis sie benötigt werden. Eine geteilte Schaltfläche kann dazu beitragen, dass ein überladenes Design minimalistisch wirkt. Bei einem erweiterten Split-Button wird möglicherweise sogar die letzte Nutzeraktion gespeichert und an die primäre Position verschoben.

Eine gängige Schaltfläche mit Drop-down-Menü finden Sie in Ihrer E‑Mail-Anwendung. Die primäre Aktion ist „Senden“. Möglicherweise können Sie die Nachricht aber auch später senden oder einen Entwurf speichern:

Eine Beispielschaltfläche „Aufteilen“ in einer E‑Mail-Anwendung.

Der Bereich für freigegebene Aktionen ist praktisch, da der Nutzer nicht suchen muss. Sie wissen, dass wichtige E‑Mail-Aktionen in der geteilten Schaltfläche enthalten sind.

Teile

Sehen wir uns die wesentlichen Bestandteile einer geteilten Schaltfläche an, bevor wir auf die Orchestrierung und die endgültige Nutzererfahrung eingehen. Das Tool zur Überprüfung der Barrierefreiheit von VisBug wird hier verwendet, um eine Makroansicht der Komponente zu erstellen und Aspekte des HTML-Codes, des Stils und der Barrierefreiheit für jeden wichtigen Teil zu präsentieren.

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

Container für die Schaltfläche „Aufteilen“ der obersten Ebene

Die Komponente der obersten Ebene ist eine 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 untersucht und die in dieser Klasse verwendeten CSS-Properties werden angezeigt.

Primäre Aktionsschaltfläche

Das anfänglich sichtbare und fokussierbare <button> passt in den Container und hat zwei passende Ecken für die Interaktionen focus, hover und active, sodass es innerhalb von .gui-split-button angezeigt wird.

Die CSS-Regeln für das Schaltflächenelement im Inspector.

Ein-/Aus-Schaltfläche für Pop-ups

Das Unterstützungselement „Pop-up-Schaltfläche“ dient zum Aktivieren und Andeuten der Liste der sekundären Schaltflächen. Beachten Sie, dass es sich nicht um ein <button> handelt und es nicht fokussierbar ist. Es ist jedoch der Positionierungsanker für .gui-popup und der Host für :focus-within, der zum Präsentieren des Pop-ups verwendet wird.

Die CSS-Regeln für die Klasse „gui-popup-button“ im Inspector.

Die Pop-up-Karte

Dies ist ein untergeordnetes Element einer schwebenden Karte, die absolut positioniert ist und die Schaltflächenliste semantisch umschließt..gui-popup-button

Im Inspector werden die CSS-Regeln für die Klasse „gui-popup“ angezeigt.

Die sekundären Aktionen

Ein fokussierbares <button> mit einer etwas kleineren Schriftgröße als die primäre Aktionsschaltfläche hat ein Symbol und einen ergänzenden Stil zur primären Schaltfläche.

Die CSS-Regeln für das Schaltflächenelement im Inspector.

Benutzerdefinierte Eigenschaften

Die folgenden Variablen helfen dabei, Farbharmonie zu schaffen und bieten einen zentralen Ort, um Werte zu ä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 wichtig, damit Screenreader die Funktion und den Status der geteilten Schaltfläche erkennen können. Das Attribut title ist für alle hilfreich.

Fügen Sie ein <svg>-Symbol und das .gui-popup-Containerelement 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 einer einfachen Pop-up-Platzierung ist .gui-popup ein untergeordnetes Element der Schaltfläche, mit der das Pop-up maximiert wird. Der einzige Haken bei dieser Strategie ist, dass im .gui-split-button-Container kein overflow: hidden verwendet werden kann, da das Pop-up sonst nicht sichtbar ist.

Ein <ul> mit <li><button>-Inhalten wird von Screenreadern als „Schaltflächenliste“ angesagt, was genau der präsentierten 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>

Um das Design aufzulockern und mit Farben zu spielen, 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 HTML und Inhalte vorhanden sind, können Sie mit Stilen Farbe und Layout festlegen.

Container für geteilte Schaltfläche gestalten

Der inline-flex-Anzeigetyp eignet sich gut für diese Wrapping-Komponente, da sie in andere geteilte Schaltflächen, Aktionen oder Elemente eingefügt 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 zum Aufteilen.

Die <button>-Formatierung

Schaltflächen sind sehr gut darin, zu verschleiern, wie viel Code erforderlich ist. Möglicherweise müssen Sie die Standardstile des Browsers rückgängig machen oder ersetzen. Außerdem müssen Sie die Vererbung erzwingen, Interaktionsstatus hinzufügen und sich an verschiedene Nutzereinstellungen und Eingabetypen anpassen. Die Anzahl der Schaltflächenstile kann schnell ansteigen.

Diese Schaltflächen unterscheiden sich von regulären Schaltflächen, da sie einen Hintergrund mit einem übergeordneten Element gemeinsam nutzen. Normalerweise hat eine Schaltfläche eine eigene Hintergrund- und Textfarbe. Diese teilen es 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 und passenden benutzerdefinierten Eigenschaften für den Status hinzu:

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

Für die primäre Schaltfläche sind einige spezielle Formatierungen erforderlich, um den gewünschten Designeffekt zu erzielen:

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

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

Außerdem haben die Schaltfläche und das Symbol für das helle Design einen Schatten erhalten:

.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 wurden die Mikrointeraktionen und kleinen Details berücksichtigt.

Hinweis zu :focus-visible

Beachten Sie, dass für die Schaltflächenstile :focus-visible anstelle von :focus verwendet wird. :focus ist ein wichtiger Schritt zur Erstellung einer barrierefreien Benutzeroberfläche, hat aber einen Nachteil: Es wird nicht berücksichtigt, ob der Nutzer die Informationen sehen muss oder nicht. Die Funktion wird bei jedem Fokus angewendet.

Im folgenden Video wird versucht, diese Mikrointeraktion aufzuschlüsseln, um zu zeigen, wie :focus-visible eine intelligente Alternative ist.

Pop-up-Schaltfläche gestalten

Eine 4ch-Flexbox zum Zentrieren eines Symbols und zum Verankern einer Pop-up-Schaltflächenliste. Wie die primäre Schaltfläche ist sie transparent, bis der Mauszeiger darauf bewegt wird oder eine Interaktion erfolgt. Sie wird so gestreckt, dass sie den gesamten Bereich ausfüllt.

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-Nesting und dem funktionalen Selektor :is() können Sie Ebenen in den Status „hover“, „focus“ und „active“ einfügen:

.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 primäre Hook zum Einblenden und Ausblenden des Pop-ups. Wenn das .gui-popup-button-Element focus für eines seiner untergeordneten Elemente enthält, legen Sie opacity, position und pointer-events für das Symbol und das Pop-up 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 die Ein- und Ausblende-Stile fertig sind, müssen Sie die Transformationen je nach Bewegungseinstellung des Nutzers bedingt überblenden:

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

Bei genauerer Betrachtung des Codes fällt auf, dass die Deckkraft weiterhin für Nutzer mit reduzierter Bewegung überblendet wird.

Pop-up gestalten

Das .gui-popup-Element ist eine Liste mit schwebenden Karten-Schaltflächen, die benutzerdefinierte Eigenschaften und relative Einheiten verwendet, um dezent kleiner zu sein. Es ist interaktiv auf die primäre Schaltfläche abgestimmt und entspricht der Marke durch die Verwendung von Farben. Die Symbole haben weniger Kontrast, sind dünner und der Schatten hat einen Hauch von Markenblau. Wie bei Schaltflächen ist ein starkes UI und UX das Ergebnis dieser kleinen 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 in jeder Karte mit dunklem und hellem Design gut aussehen:

Links und Symbole für die Kaufabwicklung, Quick Pay und „Für später speichern“.

.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 für das dunkle Design enthält zusätzlichen Text und einen zusätzlichen Symbolschatten sowie einen etwas intensiveren Schlagschatten:

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 skaliert, in der sie verwendet werden. Dazu wird die Einheit ch als inline-size verwendet. Außerdem werden für jedes Symbol einige Stile angegeben, um die Konturen weich und glatt zu gestalten.

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

Linksläufiges Layout

Logische Attribute erledigen die komplexe Arbeit. Hier ist die Liste der verwendeten logischen Eigenschaften: - display: inline-flex erstellt ein Inline-Flex-Element. – padding-block und padding-inline als Paar anstelle der Kurzform padding verwenden, um die Vorteile der Auffüllung der logischen Seiten zu nutzen. – border-end-start-radius und Freunde runden Ecken basierend auf der Dokumentrichtung. – inline-size statt width sorgt dafür, dass die Größe nicht an physische Abmessungen gebunden ist. – border-inline-start fügt am Anfang einen Rahmen hinzu, der je nach Schreibrichtung rechts oder links sein kann.

JavaScript

Fast das gesamte folgende JavaScript dient dazu, die Barrierefreiheit zu verbessern. Zwei meiner Hilfsbibliotheken werden verwendet, um die Aufgaben etwas zu vereinfachen. BlingBlingJS wird für prägnante DOM-Abfragen und die einfache Einrichtung von Event-Listenern verwendet, während roving-ux die barrierefreie Tastatur- und Gamepad-Interaktion für das Pop-up erleichtert.

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 Benutzeroberfläche zu aktualisieren.

Roving-Index

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

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

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

aria-expanded umschalten

Während es visuell offensichtlich ist, dass ein Pop-up ein- und ausgeblendet wird, benötigt ein Screenreader mehr als nur visuelle Hinweise. Hier wird JavaScript verwendet, um die CSS-gesteuerte :focus-within-Interaktion zu ergänzen, indem ein für Screenreader geeignetes Attribut umgeschaltet wird.

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

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

Escape-Taste aktivieren

Der Fokus des Nutzers wurde absichtlich auf eine Falle gelenkt. Daher müssen wir eine Möglichkeit zum Verlassen der Falle bieten. Am häufigsten wird die Verwendung des Escape-Schlüssels zugelassen. Dazu müssen Sie auf Tastendrücke auf der Pop-up-Schaltfläche achten, da alle Tastaturereignisse für untergeordnete Elemente an dieses übergeordnete Element weitergeleitet werden.

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

Wenn die Pop-up-Schaltfläche Escape-Tastendrücke erkennt, wird der Fokus mit blur() entfernt.

Klicks auf geteilte Schaltflächen

Wenn der Nutzer auf die Schaltflächen klickt, tippt oder über die Tastatur mit ihnen interagiert, muss die Anwendung die entsprechende Aktion ausführen. Auch hier wird wieder Event Bubbling verwendet, diesmal jedoch für den .gui-split-button-Container, um Schaltflächenklicks aus einem untergeordneten Pop-up oder der primären Aktion abzufangen.

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

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.

Community-Remixe