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 far sembrare minimo un design complesso. Un pulsante di suddivisione avanzato potrebbe persino ricordare l'ultima azione dell'utente e promuoverla nella posizione principale.
Un pulsante di suddivisione comune è disponibile nell'applicazione di posta elettronica. 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 di suddivisione prima di analizzare l'orchestrazione complessiva e l'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 livello più elevato è un flexbox in linea con una classe gui-split-button
contenente l'azione principale e l'.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 semplice posizionamento dei popup, .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 elemento <ul>
pieno di contenuti <li><button>
si annuncerà come "elenco di pulsanti" per gli screen reader, ovvero 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.
Definire lo stile del contenitore del pulsante diviso
Un tipo di visualizzazione inline-flex
è adatto a questo componente di a capo perché 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. Potrebbe essere necessario annullare o sostituire gli stili predefiniti del browser, ma dovrai anche applicare un'ereditarietà, aggiungere stati di interazione e adattarti a vari tipi di input e preferenze dell'utente. Gli stili dei pulsanti si accumulano rapidamente.
Questi pulsanti sono diversi dai normali pulsanti perché condividono lo sfondo con un elemento principale. In genere, un pulsante ha il proprio colore di sfondo e di testo. Queste persone, tuttavia, lo condividono e applicano solo il proprio sfondo 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 pseudo-classi CSS e utilizza le 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);
}
}
Per completare l'effetto del design, il pulsante principale richiede alcuni stili speciali:
.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 visualizzarlo o meno, ma si applica a qualsiasi opzione attivata.
Il video di seguito tenta di analizzare questa microinterazione per mostrare come
:focus-visible
sia un'alternativa intelligente.
Definizione dello stile del 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 un livello agli 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 transizioni delle trasformazioni 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 mobili di schede che utilizzano proprietà personalizzate
e unità relative che devono essere leggermente più piccole, abbinate in modo interattivo al pulsante
principale e in base al brand con 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 un'estetica gradevole 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 uniforme.
.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
arrotondano gli angoli in base alla direzione 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 imposta lo stato attivo su .gui-popup-button
, vogliamo
inoltrare lo stato attivo sul primo pulsante (o sull'ultimo pulsante selezionato) in
.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.
Attivazione/disattivazione di 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)
})
Abilitazione della chiave Escape
in corso...
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 dei 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 con i link e io la aggiungerò alla sezione dei remix della community qui sotto.