Komponente für geteilte Schaltfläche erstellen

Ein grundlegender Überblick darüber, wie du eine barrierefreie Komponente für eine geteilte Schaltfläche erstellst.

In diesem Beitrag möchte ich euch zeigen, wie ihr eine Schaltfläche zum Teilen erstellen könnt . Demo ansehen.

Demo

Falls du lieber ein Video hast, findest du hier eine YouTube-Version dieses Beitrags:

Überblick

Teilen-Schaltflächen sind Schaltflächen, die eine Hauptschaltfläche und eine Liste zusätzlicher Schaltflächen verbergen. Sie sind nützlich, um eine gängige Aktion sichtbar zu machen und gleichzeitig sekundäre, weniger häufig verwendete Aktionen zu verschachteln, bis sie benötigt werden. Eine Aufteilungsschaltfläche kann entscheidend sein, damit sich ein unübersichtliches Design minimal anfühlt. Mit der erweiterten Aufteilungsschaltfläche können Sie sich sogar die letzte Nutzeraktion merken und diese an die primäre Position verschieben.

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

Ein Beispiel für eine Schaltfläche zum Aufteilen, wie sie in einer E-Mail-Anwendung zu sehen ist.

Der Bereich mit den freigegebenen Aktionen ist gut, da sich der Nutzer nicht umsehen muss. Er weiß, dass die Schaltfläche zum Aufteilen wichtige E-Mail-Aktionen enthält.

Teile

Sehen wir uns die wesentlichen Bestandteile einer Schaltfläche zum Aufteilen genauer an, bevor wir auf ihre Gesamtorchestrierung und die User Experience eingehen. Hier wird das Tool für die Prüfung der Barrierefreiheit von VisBug verwendet, um eine Makroansicht der Komponente anzuzeigen, in der Aspekte des HTML-Codes, der Stil und die Barrierefreiheit für jeden Hauptteil angezeigt werden.

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

Container für geteilte Schaltflächen der obersten Ebene

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

Die Klasse „gui-split-button“ wurde geprüft und zeigt die in dieser Klasse verwendeten CSS-Eigenschaften an.

Die primäre Aktionsschaltfläche

Die anfänglich sichtbare und fokussierbare <button> passt in den Container mit zwei übereinstimmenden Eckenformen für Fokuss, hover und aktive Interaktionen, die in .gui-split-button enthalten sind.

Das Prüftool mit den CSS-Regeln für das Schaltflächenelement

Die Ein/Aus-Schaltfläche für das Pop-up

Das Supportelement „Pop-up-Schaltfläche“ dient dazu, die Liste der Sekundärschaltflächen zu aktivieren und darauf hinzuweisen. Sie sehen, dass es sich nicht um ein <button> handelt und es nicht fokussierbar ist. Er ist jedoch der Positionsanker für .gui-popup und den Host für :focus-within, der zum Präsentieren des Pop-ups verwendet wird.

Das Inspector-Tool, das die CSS-Regeln für die Klasse „gui-popup-button“ anzeigt.

Pop-up-Karte

Dies ist eine unverankerte Karte, die dem Anker-.gui-popup-button untergeordnet ist und absolut und semantisch die Schaltflächenliste umschließt.

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

Die sekundäre(n) Aktion(en)

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

Das Prüftool mit den CSS-Regeln für das Schaltflächenelement

Benutzerdefinierte Eigenschaften

Die folgenden Variablen helfen beim Erstellen der Farbharmonie und an einem zentralen Ort, an dem die in der Komponente verwendeten Werte geändert werden können.

@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 Farbe

Markup

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

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

Füge die Hauptschaltflä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 von entscheidender Bedeutung, um sich über die Möglichkeiten und den Status der Aufteilungsschaltflächen zu informieren. Das Attribut title 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>

Für eine einfache Pop-up-Platzierung ist .gui-popup der Schaltfläche zum Maximieren untergeordnet. Der einzige Haken bei dieser Strategie ist, dass der .gui-split-button-Container overflow: hidden nicht verwenden kann, da das Pop-up dadurch nicht mehr visuell angezeigt wird.

Ein <ul>, das mit <li><button>-Inhalten gefüllt ist, wird Screenreadern als „Schaltflächenliste“ angezeigt. Das entspricht genau der Benutzeroberfläche, die präsentiert wird.

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

Aus Gründen der Erinnerung und mit Farbe habe ich den sekundären Schaltflächen von https://heroicons.com Symbole hinzugefügt. Symbole sind sowohl für die primäre 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

Wenn HTML und Inhalte vorhanden sind, können die Stile Farben und Layout bereitstellen.

Container für die Schaltfläche „Aufteilen“ gestalten

Der Anzeigetyp inline-flex eignet sich gut für diese Wrapping-Komponente, da er in die anderen Teilen-Schaltflächen, Aktionen oder Elemente passen 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;
}

Schaltfläche „Aufteilen“

Der <button>-Stil

Schaltflächen vermitteln sehr gut einen Eindruck davon, wie viel Code erforderlich ist. Unter Umständen müssen Sie die Standardstile des Browsers rückgängig machen oder ersetzen, aber auch eine gewisse Übernahme erzwingen, Interaktionsstatus hinzufügen und die Stile an verschiedene Nutzereinstellungen und Eingabetypen anpassen. Schaltflächenstile wachsen schnell.

Diese Schaltflächen unterscheiden sich von regulären Schaltflächen, da sie denselben Hintergrund wie ein übergeordnetes Element haben. Normalerweise wird die Hintergrundfarbe und die Textfarbe einer Schaltfläche zugewiesen. 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 mit einigen CSS-Pseudoklassen Interaktionsstatus hinzu und verwenden Sie übereinstimmende benutzerdefinierte Attribute 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);
  }
}

Für die primäre Schaltfläche sind einige spezielle Stile erforderlich, 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);
  }
}

Die Schaltfläche und das Symbol des hellen Designs erhalten 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));
    }
  }
}

Eine tolle Schaltfläche hat auf die Mikrointeraktionen und die kleinen Details geachtet.

Hinweis zu :focus-visible

Für die Schaltflächenstile wird :focus-visible anstelle von :focus verwendet. :focus ist ein entscheidender Aspekt beim Erstellen einer barrierefreien Benutzeroberfläche, hat aber einen Nachteil: Es ist nicht intelligent darin, ob der Nutzer sie sehen muss oder nicht, sie gilt für jeden Fokus.

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

Pop-up-Schaltfläche gestalten

Eine 4ch-Flexbox, um ein Symbol zu zentrieren und eine Pop-up-Schaltflächenliste zu verankern. Wie die primäre Schaltfläche ist sie transparent, bis der Mauszeiger darauf bewegt wird oder mit ihr interagiert. Sie wird gestreckt, damit sie gefüllt wird.

Der Pfeilteil der Schaltfläche zum Teilen, mit der das Pop-up-Fenster 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);
}

Über die CSS-Verschachtelung und den Funktionsselektor :is() legen Sie Ebenen für den Hover-, Fokus- und aktiven Status fest:

.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 Hauptmechanismus zum Ein- und Ausblenden des Pop-ups. Wenn .gui-popup-button für eines der untergeordneten Elemente focus hat, legen Sie für das Symbol und das Pop-up opacity, 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 die In- und Out-Stile abgeschlossen sind, besteht der letzte Schritt darin, je nach Bewegungspräferenz des Nutzers bedingte Übergangstransformationen zu verwenden:

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

Wenn Sie den Code genau beobachten, stellen Sie fest, dass für Nutzer, die Bewegungen mit reduzierter Bewegung bevorzugen, die Deckkraft weiterhin umgestellt wird.

Pop-up gestalten

Das .gui-popup-Element ist eine unverankerte Schaltflächenliste einer Karte mit benutzerdefinierten Eigenschaften und relativen Einheiten, die geringfügig kleiner sind, interaktiv an die primäre Schaltfläche angepasst und durch Farbgebung auf die Marke abgestimmt werden. Die Symbole haben weniger Kontrast, sind dünner und der Schatten hat einen Hauch von Markenblau. Wie bei Schaltflächen ist eine gute UI und UX das Ergebnis dieser kleinen Details.

Ein unverankertes 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 werden mit Markenfarben versehen, um sie auf jeder Karte mit dunklen und hellen Designs ansprechend zu gestalten:

Links und Symbole für „Bezahlen“, „Schnell bezahlen“ 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%;
  }
}

Im Pop-up-Fenster mit dem dunklen Design wurden Text- und Symbolschatten sowie ein etwas intensiverer Feldschatten hinzugefügt:

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 Symbolstile für <svg>

Alle Symbole haben eine ähnliche Größe wie die Schaltfläche font-size, in der sie verwendet werden. Dabei wird die Einheit ch als inline-size verwendet. Jedem erhält auch einige Stile, die dazu beitragen, die Symbole „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 die Liste der verwendeten logischen Attribute: – display: inline-flex erstellt ein Inline-Flex-Element. – padding-block und padding-inline als Paar statt padding – das hat den Vorteil, dass die logischen Seiten aufgefüllt werden. – border-end-start-radius und Freunde runden Ecken je nach Richtung des Dokuments ab. – inline-size anstelle von 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 sich je nach Skriptrichtung auf der rechten oder linken Seite befinden kann.

JavaScript

Nahezu alle der folgenden JavaScript-Elemente dienen der Verbesserung der Barrierefreiheit. Zwei meiner Hilfsbibliotheken werden verwendet, um die Aufgaben ein wenig zu vereinfachen. BlingBlingJS wird für prägnante DOM-Abfragen und eine einfache Einrichtung des Event-Listeners verwendet. roving-ux erleichtert barrierefreie Tastatur- und Gamepad-Interaktionen für das Pop-up.

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, ist das Upgrade der Umgebung nur noch wenige Schritte abgeschlossen.

Roving-Index

Wenn eine Tastatur oder ein Screenreader den .gui-popup-button fokussiert, soll der Fokus zur ersten (oder zuletzt fokussierten) Schaltfläche im .gui-popup gelenkt werden. Die Bibliothek verwendet dazu die Parameter 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 wird umgeschaltet

Obwohl es visuell deutlich ist, dass ein Pop-up ein- und ausgeblendet wird, benötigt ein Screenreader mehr als visuelle Hinweise. JavaScript wird hier verwendet, um die CSS-gesteuerte :focus-within-Interaktion zu ergänzen, indem ein Screenreader-Attribut umgeschaltet wird.

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

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

Escape-Schlüssel wird aktiviert

Der Fokus des Nutzers wurde absichtlich auf eine Falle gelenkt, was bedeutet, dass wir eine Möglichkeit zum Verlassen anbieten müssen. Die gängigste Methode besteht darin, die Verwendung des Schlüssels Escape zuzulassen. Achten Sie dazu auf Tastendrücke auf die Pop-up-Schaltfläche, da Tastaturereignisse für untergeordnete Elemente in diesem übergeordneten Element vorkommen.

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

Wenn die Pop-up-Schaltfläche das Drücken der Escape-Taste erkennt, wird der Fokus mit blur() von sich selbst entfernt.

Klicks auf die Schaltfläche „Aufteilen“

Wenn der Nutzer schließlich auf die Schaltflächen klickt, tippt oder mit der Tastatur interagiert, muss die Anwendung die entsprechende Aktion ausführen. Hier wird noch einmal Ereignis-Bubbling verwendet, jedoch im .gui-split-button-Container, um Klicks auf Schaltflächen von 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

Jetzt weißt du, wie ich es gemacht habe. Wie würdest du es erreichen? 🙂

Diversifizieren wir unsere Ansätze und lernen Sie alle Möglichkeiten kennen, wie wir das Web nutzen können. Erstelle eine Demo und twittere mich über Links, und ich füge sie unten zum Abschnitt über Community-Remixe hinzu.

Community-Remixe