Komponente für geteilte Schaltfläche erstellen

Ein grundlegender Überblick über die Erstellung einer barrierefreien Split-Button-Komponente.

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 Schaltfläche zum erweiterten Aufteilen kann sich sogar an die letzte Nutzeraktion erinnern und sie an die primäre Position verschieben.

Eine gängige Aufteilungsschaltfläche finden Sie in Ihrer E-Mail-Anwendung. Die primäre Aktion ist „Senden“. Sie können sie 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 eine tolle Sache, da die Nutzenden sich nicht umsehen müssen. 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 Schaltfläche zum Teilen besteht.

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

Die Komponente der obersten Ebene ist ein Inline-Flexbox mit der Klasse gui-split-button, das die primäre Aktion und das .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 Unterstützungselement „Pop-up-Schaltfläche“ dient zum Aktivieren und Anführen der Liste der sekundären Schaltflächen. Beachten Sie, dass es sich nicht um ein <button> handelt und dass es nicht fokussiert werden kann. Er ist jedoch der Positionierungsanker für .gui-popup und der Host für :focus-within, der zur Darstellung des Pop-ups verwendet wird.

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

Pop-up-Karte

Dies ist ein untergeordnetes Gleitkommazahl-Element zum Anker .gui-popup-button, das absolut positioniert ist und semantisch die Schaltflächenliste umschließt.

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 Schnappschuss bei dieser Strategie ist, dass der .gui-split-button-Container overflow: hidden nicht verwenden kann, da er dazu führt, dass das Pop-up nicht mehr 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 „Teilen“.

Der <button>-Stil

Schaltflächen können sehr gut verbergen, 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. Sie 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 für die Schaltflächenstile :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 Schaltfläche zum Teilen, der zum Auslösen des Pop-ups verwendet 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 primäre Aufhänger zum 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 anwenden, je nach den Bewegungseinstellungen des Nutzers:

.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 ist eine gute 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 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. – Die Ecken von border-end-start-radius und friends runden die Ecken je nach Dokumentrichtung ab. – 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. Dieser kann sich je nach Skriptrichtung auf der rechten oder linken Seite befinden.

JavaScript

Fast das gesamte folgende JavaScript dient der Barrierefreiheit. Zwei meiner Hilfsbibliotheken erleichtern die Aufgaben. BlingBlingJS wird für kurze 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 .gui-popup-button fokussiert, soll der Fokus auf die erste (oder zuletzt fokussierte) Schaltfläche im .gui-popup weitergeleitet werden. Die Bibliothek unterstützt uns dabei mit den Parametern element und target.

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

Das Element übergibt jetzt den Fokus an die untergeordneten <button>-Ziele und aktiviert die Standardnavigation mit den Pfeiltasten zum Durchsuchen von Optionen.

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 in eine Falle gelegt, d. h., wir müssen ihm eine Möglichkeit bieten, ihn zu verlassen. Am häufigsten wird die Verwendung des Escape-Schlüssels zugelassen. Achten Sie dazu auf Tastenanschläge 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 die Schaltfläche „Aufteilen“

Wenn der Nutzer auf die Schaltflächen klickt, sie antippt oder über die Tastatur mit ihnen interagiert, muss die Anwendung die entsprechende Aktion ausführen. Hier wird wieder Ereignis-Bubbling verwendet, diesmal jedoch für den Container .gui-split-button, um Klicks auf Schaltflächen von 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, 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.

Remixe der Community