Una panoramica di base su come creare un elemento personalizzato della descrizione comando adattabile al colore e accessibile.
In questo post voglio condividere le mie idee su come creare un elemento personalizzato <tool-tip>
accessibile e adattabile al colore. Prova la demo e visualizza la fonte.
Se preferisci i video, ecco una versione di YouTube di questo post:
Panoramica
Una descrizione comando è un overlay non modale, non di blocco e non interattivo contenente informazioni supplementari sulle interfacce utente. È nascosta per impostazione predefinita e viene mostrata quando si passa il mouse sopra un elemento associato. Non è possibile selezionare o interagire direttamente con una descrizione comando. Le descrizioni comando non sostituiscono le etichette o altre informazioni di alto valore; un utente deve essere in grado di completare l'attività senza una descrizione comando.
Suggerimento di attivazione/disattivazione e descrizione comando
Come per molti componenti, esistono descrizioni diverse di una descrizione comando, ad esempio in MDN, WAI ARIA, Sarah Higley e Inclusive Componenti. Mi piace la separazione tra descrizioni comando e suggerimenti di attivazione/disattivazione. Una descrizione comando dovrebbe contenere informazioni supplementari non interattive, mentre un pulsante di attivazione/disattivazione può contenere interattività e informazioni importanti. La causa principale di questo divario è l'accessibilità, ovvero il modo in cui gli utenti si aspettano di navigare nel popup e avere accesso alle informazioni e ai pulsanti al suo interno. I suggerimenti di attivazione/disattivazione diventano rapidamente complessi.
Ecco un video di un pulsante di attivazione/disattivazione dal sito di Designcember, un overlay con interattività che l'utente può bloccare per aprirlo ed esplorare, quindi chiuderlo con il tasto per la chiusura del pulsante o il tasto Esc:
Questa GUI Challenge ha avuto come esempio una descrizione comando, cercando di fare praticamente tutto con CSS; ecco come crearla.
Markup
Ho scelto di utilizzare un elemento personalizzato <tool-tip>
. Gli autori non devono convertire
elementi personalizzati in componenti web. Il browser tratterà
<foo-bar>
come un <div>
. Un elemento personalizzato può essere paragonato
a un nomeclasse con meno specificità. Non utilizza JavaScript.
<tool-tip>A tooltip</tool-tip>
È come un div con del testo all'interno. Possiamo collegarti all'albero dell'accessibilità
degli screen reader supportati aggiungendo [role="tooltip"]
.
<tool-tip role="tooltip">A tooltip</tool-tip>
Ora per gli screen reader viene riconosciuta come descrizione comando. Nell'esempio che segue, come il primo elemento link presenta un elemento della descrizione comando riconosciuto nella struttura e il secondo no? La seconda non ha questo ruolo. Nella sezione Stili miglioreremo questa visualizzazione ad albero.
Abbiamo quindi bisogno che la descrizione comando non sia attivabile. Se uno screen reader non
comprende il ruolo della descrizione comando, gli utenti potranno impostare <tool-tip>
per leggere i contenuti, senza che sia necessario per l'esperienza utente. Gli screen reader aggiungono i contenuti all'elemento principale e, come tali, non è necessario che lo stato attivo sia reso accessibile. Qui possiamo utilizzare inert
per assicurarci che nessun utente trovi accidentalmente questi contenuti della descrizione comando nel flusso delle schede:
<tool-tip inert role="tooltip">A tooltip</tool-tip>
Ho quindi scelto di utilizzare gli attributi come interfaccia per specificare la posizione della descrizione comando. Per impostazione predefinita, tutti gli elementi <tool-tip>
assumono la posizione "in alto", ma la posizione può essere personalizzata su un elemento aggiungendo tip-position
:
<tool-tip role="tooltip" tip-position="right ">A tooltip</tool-tip>
Per cose come questo tendo a utilizzare gli attributi invece delle classi, in modo che
<tool-tip>
non possa avere più posizioni assegnate contemporaneamente.
Può esserci un solo elemento o nessuno.
Infine, posiziona gli elementi <tool-tip>
all'interno dell'elemento per il quale vuoi fornire una descrizione comando. Qui condivido il testo alt
con gli utenti vedenti posizionando un'immagine e un <tool-tip>
all'interno di un elemento <picture>
:
<picture>
<img alt="The GUI Challenges skull logo" width="100" src="...">
<tool-tip role="tooltip" tip-position="bottom">
The <b>GUI Challenges</b> skull logo
</tool-tip>
</picture>
Qui inserisco un elemento <tool-tip>
all'interno di un elemento
<abbr>
:
<p>
The <abbr>HTML <tool-tip role="tooltip" tip-position="top">Hyper Text Markup Language</tool-tip></abbr> abbr element.
</p>
Accessibilità
Dal momento che ho scelto di creare descrizioni comando e non suggerimenti di attivazione/disattivazione, questa sezione è molto più semplice. Innanzitutto, illustrerò quale sia l'esperienza utente che desideriamo offrire:
- In spazi ristretti o nelle interfacce caotiche, nascondi i messaggi supplementari.
- Quando un utente passa il mouse sopra un elemento, lo mette a fuoco o lo usa per interagire con un elemento, mostra il messaggio.
- Quando passi il mouse, lo stato attivo o il tocco termina, nascondi di nuovo il messaggio.
- Infine, assicurati che il movimento sia ridotto se un utente ha specificato una preferenza per il movimento ridotto.
Il nostro obiettivo è la messaggistica supplementare on demand. Una persona vedente il mouse o la tastiera può passare il mouse per visualizzare il messaggio e leggerlo con gli occhi. Una persona non vedente con uno screen reader può concentrarsi sulla rivelazione del messaggio, udindone la ricezione attraverso il proprio strumento.
Nella sezione precedente abbiamo parlato dell'albero dell'accessibilità, del ruolo della descrizione comando e dell'inerte. Quello che rimane da fare è testarlo e verificare l'esperienza utente in modo appropriato per mostrare il messaggio della descrizione comando all'utente. Al momento del test, non è chiaro quale parte del messaggio sonoro sia la descrizione comando. Può essere visto anche durante il debug nell'albero dell'accessibilità e il testo del link "top" viene eseguito insieme, senza esitazioni, con "Guarda, descrizioni comando!". Lo screen reader non spezza e non identifica il testo come contenuto della descrizione comando.
Aggiungi uno pseudo elemento solo per screen reader a <tool-tip>
e possiamo aggiungere il testo del nostro prompt per gli utenti non vedenti.
&::before {
content: "; Has tooltip: ";
clip: rect(1px, 1px, 1px, 1px);
clip-path: inset(50%);
height: 1px;
width: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
}
Di seguito puoi vedere l'albero dell'accessibilità aggiornato, che ora ha un punto e virgola dopo il testo del link e una richiesta per la descrizione comando "Ha una descrizione comando: ".
Ora, quando un utente di screen reader mette a fuoco il link, dice "in alto" e fa una piccola pausa, poi annuncia "con descrizione comando: guarda, descrizioni comando". Ciò offre all'utente di uno screen reader un paio di utili suggerimenti UX. L'esitazione fornisce una bella separazione tra il testo del link e la descrizione comando. Inoltre, quando viene annunciato "presenta la descrizione comando", l'utente di uno screen reader può annullarla facilmente se l'ha già sentita prima. Ricorda il passaggio rapido e l'annullamento del mouse, come hai già visto il messaggio supplementare. Sembrava una bella parità UX.
Stili
L'elemento <tool-tip>
sarà un elemento secondario dell'elemento che rappresenta
il messaggio supplementare, quindi iniziamo con gli elementi essenziali per
l'effetto overlay. Esci dal flusso dei documenti con position absolute
:
tool-tip {
position: absolute;
z-index: 1;
}
Se l'elemento padre non è un contesto di stack, la descrizione comando si posizionerà a
quello più vicino, che non è ciò che vogliamo. È disponibile un nuovo selettore sul
blocco che può aiutare, :has()
:
:has(> tool-tip) {
position: relative;
}
Non preoccuparti troppo del supporto del browser. Innanzitutto, le descrizioni comando
sono supplementari. Se non funzionano, dovrebbe andare bene. In secondo luogo, nella sezione JavaScript eseguiremo il deployment di uno script per il polyfill della funzionalità necessaria per i browser che non supportano :has()
.
Quindi, rendi le descrizioni comando non interattive in modo che non rubano eventi di puntatore dall'elemento principale:
tool-tip {
…
pointer-events: none;
user-select: none;
}
Poi, nascondi la descrizione comando con un'opacità in modo da poterla eseguire con una dissolvenza incrociata:
tool-tip {
opacity: 0;
}
:has(> tool-tip):is(:hover, :focus-visible, :active) > tool-tip {
opacity: 1;
}
:is()
e :has()
svolgono il lavoro più impegnativo qui, informando tool-tip
, contenenti gli elementi principali, dell'interattività dell'utente per attivare/disattivare la visibilità di una descrizione comando secondaria. Gli utenti di mouse
possono passare il mouse, gli utenti di tastiera e screen reader possono impostare lo stato attivo e gli utenti di tocco possono toccare.
Dato che l'overlay "Mostra/Nascondi" è disponibile per gli utenti vedenti, è il momento di aggiungere alcuni stili per la tematizzazione, il posizionamento e l'aggiunta della forma triangolare alla bolla. I seguenti stili iniziano a utilizzare proprietà personalizzate, a partire dalla posizione attuale, ma anche aggiungendo ombre, tipografia e colori in modo che appaia come una descrizione comando mobile:
tool-tip {
--_p-inline: 1.5ch;
--_p-block: .75ch;
--_triangle-size: 7px;
--_bg: hsl(0 0% 20%);
--_shadow-alpha: 50%;
--_bottom-tip: conic-gradient(from -30deg at bottom, rgba(0,0,0,0), #000 1deg 60deg, rgba(0,0,0,0) 61deg) bottom / 100% 50% no-repeat;
--_top-tip: conic-gradient(from 150deg at top, rgba(0,0,0,0), #000 1deg 60deg, rgba(0,0,0,0) 61deg) top / 100% 50% no-repeat;
--_right-tip: conic-gradient(from -120deg at right, rgba(0,0,0,0), #000 1deg 60deg, rgba(0,0,0,0) 61deg) right / 50% 100% no-repeat;
--_left-tip: conic-gradient(from 60deg at left, rgba(0,0,0,0), #000 1deg 60deg, rgba(0,0,0,0) 61deg) left / 50% 100% no-repeat;
pointer-events: none;
user-select: none;
opacity: 0;
transform: translateX(var(--_x, 0)) translateY(var(--_y, 0));
transition: opacity .2s ease, transform .2s ease;
position: absolute;
z-index: 1;
inline-size: max-content;
max-inline-size: 25ch;
text-align: start;
font-size: 1rem;
font-weight: normal;
line-height: normal;
line-height: initial;
padding: var(--_p-block) var(--_p-inline);
margin: 0;
border-radius: 5px;
background: var(--_bg);
color: CanvasText;
will-change: filter;
filter:
drop-shadow(0 3px 3px hsl(0 0% 0% / var(--_shadow-alpha)))
drop-shadow(0 12px 12px hsl(0 0% 0% / var(--_shadow-alpha)));
}
/* create a stacking context for elements with > tool-tips */
:has(> tool-tip) {
position: relative;
}
/* when those parent elements have focus, hover, etc */
:has(> tool-tip):is(:hover, :focus-visible, :active) > tool-tip {
opacity: 1;
transition-delay: 200ms;
}
/* prepend some prose for screen readers only */
tool-tip::before {
content: "; Has tooltip: ";
clip: rect(1px, 1px, 1px, 1px);
clip-path: inset(50%);
height: 1px;
width: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
}
/* tooltip shape is a pseudo element so we can cast a shadow */
tool-tip::after {
content: "";
background: var(--_bg);
position: absolute;
z-index: -1;
inset: 0;
mask: var(--_tip);
}
/* top tooltip styles */
tool-tip:is(
[tip-position="top"],
[tip-position="block-start"],
:not([tip-position]),
[tip-position="bottom"],
[tip-position="block-end"]
) {
text-align: center;
}
Aggiustamenti del tema
La descrizione comando ha solo pochi colori da gestire poiché il colore del testo viene ereditato dalla pagina tramite la parola chiave di sistema CanvasText
. Inoltre, poiché abbiamo creato proprietà personalizzate per archiviare i valori, possiamo aggiornare solo queste proprietà e lasciare che sia il tema a occuparsi del resto:
@media (prefers-color-scheme: light) {
tool-tip {
--_bg: white;
--_shadow-alpha: 15%;
}
}
Per il tema chiaro, adattiamo lo sfondo al bianco e rendiamo le ombre meno marcate regolandone l'opacità.
Da destra a sinistra
Per supportare le modalità di lettura da destra a sinistra, una proprietà personalizzata archivierà il valore della direzione del documento in un valore rispettivamente di -1 o 1.
tool-tip {
--isRTL: -1;
}
tool-tip:dir(rtl) {
--isRTL: 1;
}
Può essere utilizzato per posizionare la descrizione comando:
tool-tip[tip-position="top"]) {
--_x: calc(50% * var(--isRTL));
}
Oltre a contribuire alla posizione del triangolo:
tool-tip[tip-position="right"]::after {
--_tip: var(--_left-tip);
}
tool-tip[tip-position="right"]:dir(rtl)::after {
--_tip: var(--_right-tip);
}
Infine, può essere utilizzato anche per le trasformazioni logiche su translateX()
:
--_x: calc(var(--isRTL) * -3px * -1);
Posizionamento descrizione comando
Posiziona la descrizione comando in modo logico con le proprietà inset-block
o inset-inline
per gestire le posizioni fisiche e logiche della descrizione comando. Il seguente codice mostra lo stile per ciascuna delle quattro posizioni sia per le direzioni da sinistra a destra che da destra a sinistra.
Allineamento in alto e inizio blocco
tool-tip:is([tip-position="top"], [tip-position="block-start"], :not([tip-position])) {
inset-inline-start: 50%;
inset-block-end: calc(100% + var(--_p-block) + var(--_triangle-size));
--_x: calc(50% * var(--isRTL));
}
tool-tip:is([tip-position="top"], [tip-position="block-start"], :not([tip-position]))::after {
--_tip: var(--_bottom-tip);
inset-block-end: calc(var(--_triangle-size) * -1);
border-block-end: var(--_triangle-size) solid transparent;
}
Allineamento a destra e in linea alla fine
tool-tip:is([tip-position="right"], [tip-position="inline-end"]) {
inset-inline-start: calc(100% + var(--_p-inline) + var(--_triangle-size));
inset-block-end: 50%;
--_y: 50%;
}
tool-tip:is([tip-position="right"], [tip-position="inline-end"])::after {
--_tip: var(--_left-tip);
inset-inline-start: calc(var(--_triangle-size) * -1);
border-inline-start: var(--_triangle-size) solid transparent;
}
tool-tip:is([tip-position="right"], [tip-position="inline-end"]):dir(rtl)::after {
--_tip: var(--_right-tip);
}
Allineamento in basso e alla fine del blocco
tool-tip:is([tip-position="bottom"], [tip-position="block-end"]) {
inset-inline-start: 50%;
inset-block-start: calc(100% + var(--_p-block) + var(--_triangle-size));
--_x: calc(50% * var(--isRTL));
}
tool-tip:is([tip-position="bottom"], [tip-position="block-end"])::after {
--_tip: var(--_top-tip);
inset-block-start: calc(var(--_triangle-size) * -1);
border-block-start: var(--_triangle-size) solid transparent;
}
Allineamento a sinistra e all'inizio in linea
tool-tip:is([tip-position="left"], [tip-position="inline-start"]) {
inset-inline-end: calc(100% + var(--_p-inline) + var(--_triangle-size));
inset-block-end: 50%;
--_y: 50%;
}
tool-tip:is([tip-position="left"], [tip-position="inline-start"])::after {
--_tip: var(--_right-tip);
inset-inline-end: calc(var(--_triangle-size) * -1);
border-inline-end: var(--_triangle-size) solid transparent;
}
tool-tip:is([tip-position="left"], [tip-position="inline-start"]):dir(rtl)::after {
--_tip: var(--_left-tip);
}
Animazione
Finora abbiamo attivato solo la visibilità della descrizione comando. In questa sezione, innanzitutto animano l'opacità per tutti gli utenti, poiché si tratta di una transizione a movimento ridotto in genere sicura. Successivamente, annulleremo la posizione della trasformazione in modo che la descrizione comando scorra fuori dall'elemento principale.
Una transizione predefinita sicura e significativa
Applica uno stile all'elemento della descrizione comando in modo da opacità e trasformazione della transizione, in questo modo:
tool-tip {
opacity: 0;
transform: translateX(var(--_x, 0)) translateY(var(--_y, 0));
transition: opacity .2s ease, transform .2s ease;
}
:has(> tool-tip):is(:hover, :focus-visible, :active) > tool-tip {
opacity: 1;
transition-delay: 200ms;
}
Aggiungere movimento alla transizione
Per ogni lato su cui può essere visualizzata una descrizione comando. Se l'utente è d'accordo con il movimento, posiziona leggermente la proprietà TraduttoreX dando una piccola distanza da cui percorrere una distanza:
@media (prefers-reduced-motion: no-preference) {
:has(> tool-tip:is([tip-position="top"], [tip-position="block-start"], :not([tip-position]))):not(:hover):not(:focus-visible):not(:active) tool-tip {
--_y: 3px;
}
:has(> tool-tip:is([tip-position="right"], [tip-position="inline-end"])):not(:hover):not(:focus-visible):not(:active) tool-tip {
--_x: -3px;
}
:has(> tool-tip:is([tip-position="bottom"], [tip-position="block-end"])):not(:hover):not(:focus-visible):not(:active) tool-tip {
--_y: -3px;
}
:has(> tool-tip:is([tip-position="left"], [tip-position="inline-start"])):not(:hover):not(:focus-visible):not(:active) tool-tip {
--_x: 3px;
}
}
Tieni presente che questa opzione è impostata sullo stato "out", poiché lo stato "in" è impostato su translateX(0)
.
JavaScript
A mio parere il codice JavaScript è facoltativo. Il motivo è che nessuna di queste descrizioni comando dovrebbe essere obbligatoria per svolgere un'attività nella tua UI. Quindi, se le descrizioni comando non funzionano del tutto, non è un problema. Ciò significa anche che possiamo trattare
le descrizioni comando come perfezionate progressivamente. Alla fine tutti i browser supporteranno
:has()
e questo script potrebbe essere eliminato completamente.
Lo script di polyfill ha due funzioni e lo fa solo se il browser non supporta :has()
. Innanzitutto, cerca l'assistenza per :has()
:
if (!CSS.supports('selector(:has(*))')) {
// do work
}
Quindi, trova gli elementi principali di <tool-tip>
e assegna loro un classname con cui lavorare:
if (!CSS.supports('selector(:has(*))')) {
document.querySelectorAll('tool-tip').forEach(tooltip =>
tooltip.parentNode.classList.add('has_tool-tip'))
}
Quindi, inserisci un insieme di stili che utilizzano quel nome di classe, simulando il selettore :has()
per lo stesso comportamento:
if (!CSS.supports('selector(:has(*))')) {
document.querySelectorAll('tool-tip').forEach(tooltip =>
tooltip.parentNode.classList.add('has_tool-tip'))
let styles = document.createElement('style')
styles.textContent = `
.has_tool-tip {
position: relative;
}
.has_tool-tip:is(:hover, :focus-visible, :active) > tool-tip {
opacity: 1;
transition-delay: 200ms;
}
`
document.head.appendChild(styles)
}
È tutto. Ora tutti i browser mostreranno le descrizioni comando se :has()
non è supportato.
Conclusione
Ora che sai come ci sono riuscito, cosa faresti? 🙂 Non vedo l'ora
dell'API
popup
per semplificare i suggerimenti di attivazione/disattivazione, del livello
superiore per evitare battaglie z-index e dell'API
anchor
per posizionare meglio gli elementi nella finestra. Fino ad allora, ti fornirò
le descrizioni comando.
Diversifica i nostri approcci e scopriamo tutti i modi per creare sul web.
Crea una demo, inviami un tweet con i link e lo aggiungerò alla sezione Remix della community di seguito.
Remix della community
Ancora niente da visualizzare.
Risorse
- Codice sorgente su GitHub