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