Una panoramica di base su come creare un componente di pulsante diviso accessibile.
In questo post voglio condividere alcune idee su come creare un pulsante diviso . Prova la demo.
Se preferisci i video, ecco una versione di questo post su YouTube:
Panoramica
I pulsanti suddivisi sono pulsanti che nascondono un pulsante principale e un elenco di pulsanti aggiuntivi. Sono utili per mostrare un'azione comune e nidificare azioni secondarie meno utilizzate fino a quando non sono necessarie. Un pulsante diviso può essere fondamentale per rendere un design elaborato più minimalista. Un pulsante di suddivisione avanzato potrebbe persino ricordare l'ultima azione dell'utente e promuoverla nella posizione principale.
Un pulsante di separazione comune è disponibile nell'applicazione email. L'azione principale è Invia, ma potresti inviare il messaggio in un secondo momento o salvare una bozza:
L'area di azioni condivisa è utile, perché l'utente non deve guardarsi intorno. Lo fanno sapendo che le azioni email essenziali sono contenute nel pulsante diviso.
Parti
Analizziamo le parti essenziali di un pulsante diviso prima di discutere della loro orchestrazione complessiva e dell'esperienza utente finale. Lo strumento di controllo dell'accessibilità di VisBug viene utilizzato qui per mostrare una visualizzazione macro del componente, evidenziando aspetti dell'HTML, dello stile e dell'accessibilità per ogni parte principale.
Contenitore del pulsante di suddivisione di primo livello
Il componente di primo livello è un flexbox in linea, con una classe gui-split-button
, contenente l'azione principale e .gui-popup-button
.
Il pulsante di azione principale
<button>
, inizialmente visibile e attivabile, si inserisce nel contenitore con due forme angolari corrispondenti per le interazioni di attivazione, tasto Maiusc e attiva in modo che appaiano contenute in .gui-split-button
.
Pulsante di attivazione/disattivazione del popup
L'elemento di supporto "pulsante popup" serve per attivare e fare riferimento all'elenco dei pulsanti secondari. Tieni presente che non è un <button>
e non è possibile acquisire il relativo focus. Tuttavia, è l'ancora di posizionamento per .gui-popup
e l'host per :focus-within
utilizzato per presentare il popup.
La scheda popup
Si tratta di una scheda secondaria mobile rispetto all'elemento di ancoraggio
.gui-popup-button
, posizionata in modo assoluto e
che avvolge semanticamente l'elenco di pulsanti.
Le azioni secondarie
Un <button>
attivabile con una dimensione del carattere leggermente inferiore rispetto al pulsante di azione principale presenta un'icona e uno stile complementare al pulsante principale.
Proprietà personalizzate
Le seguenti variabili consentono di creare armonia di colori e un punto di riferimento per modificare i valori utilizzati in tutto il componente.
@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%);
}
}
Layout e colore
Segni e linee
L'elemento inizia come <div>
con un nome di classe personalizzato.
<div class="gui-split-button"></div>
Aggiungi il pulsante principale e gli elementi .gui-popup-button
.
<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>
Tieni presente gli attributi aria aria-haspopup
e aria-expanded
. Questi indicatori sono fondamentali per consentire agli screen reader di conoscere la funzionalità e lo stato dell'esperienza con i pulsanti suddivisi. L'attributo title
è utile per tutti.
Aggiungi un'icona <svg>
e l'elemento contenitore .gui-popup
.
<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>
Per un posizionamento popup semplice, .gui-popup
è un elemento secondario del pulsante che lo espande. L'unico problema di questa strategia è che il contenitore .gui-split-button
non può utilizzare overflow: hidden
, in quanto il popup non sarà visualizzato.
Un <ul>
compilato con contenuti <li><button>
verrà annunciato come "elenco di pulsanti" agli screen reader, che è esattamente l'interfaccia presentata.
<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>
Per un tocco di stile e per divertirti con i colori, ho aggiunto icone ai pulsanti secondari provenienti da https://heroicons.com. Le icone sono facoltative sia per i pulsanti principali sia per quelli secondari.
<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>
Stili
Una volta impostati HTML e contenuti, gli stili sono pronti per fornire colore e layout.
Aggiungere stili al contenitore del pulsante diviso
Un tipo di visualizzazione inline-flex
è adatto a questo componente di a capo, in quanto deve essere in linea con altri pulsanti, azioni o elementi suddivisi.
.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;
}
Lo stile <button>
I pulsanti sono molto efficaci per nascondere la quantità di codice richiesta. Potresti dover annullare o sostituire gli stili predefiniti del browser, ma dovrai anche applicare alcune regole di ereditarietà, aggiungere stati di interazione e adattarti a varie preferenze utente e tipi di input. Gli stili dei pulsanti si accumulano rapidamente.
Questi pulsanti sono diversi dai pulsanti normali perché condividono un sfondo con un elemento principale. In genere, un pulsante ha il proprio colore di sfondo e di testo. Tuttavia, questi lo condividono e applicano il proprio sfondo solo all'interazione.
.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;
}
Aggiungi stati di interazione con alcune pseudoclassi CSS e utilizza proprietà personalizzate corrispondenti per lo stato:
.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);
}
}
Il pulsante principale richiede alcuni stili speciali per completare l'effetto di design:
.gui-split-button > button {
border-end-start-radius: var(--radius);
border-start-start-radius: var(--radius);
& > svg {
fill: none;
stroke: var(--ontheme);
}
}
Infine, per un tocco di stile, il pulsante e l'icona del tema chiaro hanno un'ombra:
.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));
}
}
}
Un pulsante eccellente è stato realizzato prestando attenzione alle microinterazioni e ai piccoli dettagli.
Una nota su :focus-visible
Nota come gli stili dei pulsanti utilizzano :focus-visible
anziché :focus
. :focus
è un tocco fondamentale per creare un'interfaccia utente accessibile, ma ha un
svantaggio: non è intelligente nel capire se l'utente deve visualizzarla o meno, ma si applica a qualsiasi opzione di messa a fuoco.
Il video di seguito tenta di analizzare questa microinterazione per mostrare come
:focus-visible
sia un'alternativa intelligente.
Aggiungere uno stile al pulsante popup
Un 4ch
flexbox per centrare un'icona e ancorare un elenco di pulsanti popup. Come il pulsante principale, è trasparente finché non si passa il mouse sopra o non si interagisce con esso e si estende per riempire lo spazio.
.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);
}
Applica gli stati di passaggio del mouse, di attivazione e attivo con il nidificazione CSS e il selettore funzionale
:is()
:
.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);
}
}
Questi stili sono l'elemento principale per mostrare e nascondere il popup. Quando .gui-popup-button
ha focus
su uno dei suoi elementi secondari, imposta opacity
, la posizione e pointer-events
sull'icona e sul popup.
.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;
}
}
}
Una volta completati gli stili in e out, l'ultimo passaggio consiste nel applicare le trasformazioni di transizione in modo condizionale in base alle preferenze di movimento dell'utente:
.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;
}
}
}
Un occhio attento al codice noterà che l'opacità è ancora in transizione per gli utenti che preferiscono ridurre il movimento.
Stilizzazione del popup
L'elemento .gui-popup
è un elenco di pulsanti di schede fluttuanti che utilizza proprietà personalizzate e unità relative per essere leggermente più piccolo, abbinato in modo interattivo al pulsante principale e in linea con il brand per l'uso del colore. Notare che le icone hanno meno contrasto, sono più sottili e l'ombra ha un tocco di blu del brand. Come per i pulsanti,
un'interfaccia utente e un'esperienza utente efficaci sono il risultato dell'accumulo di questi piccoli dettagli.
.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%))
;
}
Le icone e i pulsanti sono assegnati ai colori del brand per creare uno stile accattivante in ogni scheda con tema scuro e chiaro:
.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%;
}
}
Il popup con tema scuro ha aggiunte di ombre di testo e icone, oltre a un'ombra della casella leggermente più intensa:
.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));
}
}
}
Stili di icone generiche <svg>
Tutte le icone hanno dimensioni relative al pulsante font-size
in cui vengono utilizzate utilizzando l'unità ch
come inline-size
. A ciascuna è stato anche assegnato uno stile per aiutare a delineare le icone in modo morbido e scorrevole.
.gui-split-button svg {
inline-size: 2ch;
box-sizing: content-box;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 2px;
}
Layout da destra a sinistra
Le proprietà logiche eseguono tutto il lavoro complesso.
Di seguito è riportato l'elenco delle proprietà logiche utilizzate:
- display: inline-flex
crea un elemento flex in linea.
- padding-block
e padding-inline
come coppia, anziché la rappresentazione abbreviata padding
, per usufruire dei vantaggi dell'aggiunta di spazi ai lati logici.
- border-end-start-radius
e
amici arrotonderanno i bordi in base all'orientamento del documento.
- inline-size
anziché width
garantisce che le dimensioni non siano legate alle dimensioni fisiche.
- border-inline-start
aggiunge un bordo all'inizio, che può essere a destra o a sinistra a seconda della direzione dello script.
JavaScript
Quasi tutto il codice JavaScript seguente serve a migliorare l'accessibilità. Due delle mie librerie di supporto vengono utilizzate per semplificare le attività. BlingBlingJS viene utilizzato per query DOM concise e una facile configurazione degli ascoltatori di eventi, mentre roving-ux contribuisce a facilitare le interazioni accessibili con tastiera e gamepad per il popup.
import $ from 'blingblingjs'
import {rovingIndex} from 'roving-ux'
const splitButtons = $('.gui-split-button')
const popupButtons = $('.gui-popup-button')
Dopo aver importato le librerie precedenti e aver selezionato e salvato gli elementi nelle variabili, l'upgrade dell'esperienza manca solo di alcune funzioni.
Indice mobile
Quando una tastiera o uno screen reader attiva lo stato attivo su .gui-popup-button
, vogliamo spostarlo sul primo pulsante (o su quello attivato più di recente) nel .gui-popup
. La libreria ci aiuta a farlo con i parametri element
e target
.
popupButtons.forEach(element =>
rovingIndex({
element,
target: 'button',
}))
Ora l'elemento passa il focus agli elementi secondari <button>
target e consente di navigare tra le opzioni con i tasti freccia standard.
Attiva/disattiva aria-expanded
Sebbene sia visivamente evidente che un popup viene visualizzato e nascosto, uno screen reader ha bisogno di più di semplici indicatori visivi. JavaScript viene utilizzato qui per integrare l'interazione :focus-within
basata su CSS attivando/disattivando un attributo appropriato per lo screen reader.
popupButtons.on('focusin', e => {
e.currentTarget.setAttribute('aria-expanded', true)
})
popupButtons.on('focusout', e => {
e.currentTarget.setAttribute('aria-expanded', false)
})
Attivazione della chiave Escape
L'attenzione dell'utente è stata intenzionalmente indirizzata a una trappola, il che significa che dobbiamo fornire un modo per uscire. Il modo più comune è consentire l'utilizzo della chiave Escape
.
A tal fine, controlla le pressioni dei tasti sul pulsante popup, poiché tutti gli eventi relativi alla tastiera sugli elementi figli verranno visualizzati in questo elemento principale.
popupButtons.on('keyup', e => {
if (e.code === 'Escape')
e.target.blur()
})
Se il pulsante popup rileva una pressione del tasto Escape
, rimuove il proprio stato attivo con blur()
.
Clic sul pulsante di suddivisione
Infine, se l'utente fa clic, tocca o la tastiera interagisce con i pulsanti, l'applicazione deve eseguire l'azione appropriata. Qui viene utilizzato nuovamente il bubbling degli eventi, ma questa volta sul contenitore .gui-split-button
per rilevare i clic sul pulsante da un popup secondario o dall'azione principale.
splitButtons.on('click', event => {
if (event.target.nodeName !== 'BUTTON') return
console.info(event.target.innerText)
})
Conclusione
Ora che sai come ho fatto, come faresti? 🙂
Diversifichiamo i nostri approcci e impariamo tutti i modi per creare sul web. Crea una demo, twittami i link e io la aggiungerò alla sezione dei remix della community di seguito.