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 ai colori. Prova la
demo e visualizza il
codice sorgente.
Se preferisci i video, ecco una versione di questo post su YouTube:
Panoramica
Una descrizione comando è una sovrapposizione non modale, non bloccante e non interattiva che contiene informazioni supplementari per le interfacce utente. È nascosto per impostazione predefinita e viene mostrato quando un elemento associato viene passato con il mouse o selezionato. Non è possibile selezionare o interagire direttamente con una descrizione comando. I suggerimenti non sostituiscono le etichette o altre informazioni di alto valore. Un utente deve essere in grado di completare la propria attività senza un suggerimento.

Non: fare affidamento sulle descrizioni comando anziché sulle etichette
Toggletip e Descrizione comando
Come per molti componenti, esistono descrizioni diverse di che cos'è una descrizione comando, ad esempio in MDN, WAI ARIA, Sarah Higley e Inclusive Components. Mi piace la separazione tra descrizioni comando e pulsanti di attivazione/disattivazione. Una descrizione comando deve contenere informazioni supplementari non interattive, mentre un toggletip può contenere interattività e informazioni importanti. Il motivo principale della divisione è l'accessibilità: come si prevede che gli utenti accedano al popup e abbiano accesso alle informazioni e ai pulsanti al suo interno. I suggerimenti a comparsa diventano rapidamente complessi.
Ecco un video di un suggerimento a comparsa del sito Designcember: un overlay con interattività che un utente può aprire ed esplorare, poi chiudere con un semplice tocco o il tasto Esci:
Questa sfida della GUI ha seguito il percorso di una descrizione comando, cercando di fare quasi tutto con CSS. Ecco come realizzarla.
Segni e linee
Ho scelto di utilizzare un elemento personalizzato <tool-tip>
. Gli autori non devono trasformare gli elementi personalizzati in componenti web se non vogliono. Il browser tratterà
<foo-bar>
come un <div>
. Puoi considerare un elemento personalizzato come una
classe con meno specificità. Non è coinvolto JavaScript.
<tool-tip>A tooltip</tool-tip>
È come un div con del testo all'interno. Possiamo integrarlo nell'albero di accessibilità
di screen reader compatibili aggiungendo [role="tooltip"]
.
<tool-tip role="tooltip">A tooltip</tool-tip>
Ora, per gli screen reader, viene riconosciuto come una descrizione comando. Nell'esempio seguente, il primo elemento di collegamento ha un elemento della descrizione comando riconosciuto nel suo albero, mentre il secondo no. Il secondo non ha il ruolo. Nella sezione degli stili miglioreremo questa visualizzazione ad albero.
Poi dobbiamo fare in modo che la descrizione comando non sia selezionabile. Se uno screen reader non
comprende il ruolo della descrizione comando, consentirà agli utenti di mettere a fuoco <tool-tip>
per
leggerne i contenuti, ma l'esperienza utente non ne ha bisogno. Gli screen reader
aggiungeranno i contenuti all'elemento principale e, pertanto, non è necessario
che venga reso accessibile. Qui possiamo utilizzare inert
per assicurarci che nessun utente
trovi accidentalmente i contenuti di questo suggerimento 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 i <tool-tip>
assumono una 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>
Tendo a utilizzare gli attributi anziché le classi per questo tipo di cose, in modo che
<tool-tip>
non possa avere più posizioni assegnate contemporaneamente.
Può essercene uno solo o nessuno.
Infine, inserisci gli elementi <tool-tip>
all'interno dell'elemento per cui vuoi fornire una
descrizione comando. Qui condivido il testo alt
con gli utenti vedenti inserendo 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 <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à
Poiché ho scelto di creare descrizioni comando e non descrizioni comando attivate/disattivate, questa sezione è molto più semplice. Innanzitutto, vorrei descrivere l'esperienza utente che vogliamo offrire:
- In spazi ristretti o interfacce disordinate, nascondi i messaggi supplementari.
- Quando un utente passa il mouse sopra un elemento, lo seleziona o lo tocca, viene visualizzato il messaggio.
- Quando il passaggio del mouse, lo stato attivo o il tocco termina, nascondi di nuovo il messaggio.
- Infine, assicurati che qualsiasi movimento sia ridotto se un utente ha specificato una preferenza per movimenti ridotti.
Il nostro obiettivo è la messaggistica supplementare on demand. Un utente che utilizza il mouse o la tastiera può passare il cursore sopra il messaggio per visualizzarlo e leggerlo. Un utente di screen reader non vedente può mettere a fuoco per visualizzare il messaggio, ricevendo la notifica tramite il suo strumento.

Nella sezione precedente abbiamo trattato l'albero dell'accessibilità, il ruolo della descrizione comando e inert. Non resta che testare e verificare che l'esperienza utente riveli in modo appropriato il messaggio della descrizione comando all'utente. Durante il test, non è chiaro quale parte del messaggio udibile sia una descrizione comando. Può essere visto anche durante il debug nell'albero dell'accessibilità, il testo del link "top" è unito, senza esitazione, a "Look, tooltips!". Lo screen reader non interrompe né identifica il testo come contenuto della descrizione comando.
Aggiungi uno pseudo-elemento solo per screen reader a <tool-tip>
e possiamo aggiungere il nostro
testo del 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 un prompt per la descrizione comando "Has tooltip: ".
Ora, quando un utente di screen reader mette a fuoco il link, viene pronunciata la parola "in alto", segue una breve pausa e poi viene annunciato "ha tooltip: guarda, tooltip". In questo modo, l'utente di uno screen reader riceve un paio di suggerimenti UX utili. L'esitazione crea una separazione tra il testo del link e la descrizione comando. Inoltre, quando viene annunciato "ha descrizione comando", un utente di screen reader può annullarlo facilmente se lo ha già sentito. Ricorda molto l'hover e l'unhover rapidi, dato che hai già visto il messaggio supplementare. Mi è sembrato un buon modo per raggiungere la parità dell'esperienza utente.
Stili
L'elemento <tool-tip>
sarà un elemento secondario dell'elemento per cui rappresenta
i messaggi supplementari, quindi iniziamo con gli elementi essenziali per l'effetto
di sovrapposizione. Rimuovilo dal flusso di documenti con position absolute
:
tool-tip {
position: absolute;
z-index: 1;
}
Se l'elemento principale non è un contesto di sovrapposizione, il suggerimento si posizionerà su quello più vicino, il che non è quello che vogliamo. Nel blocco è presente un nuovo selettore che può aiutarti, :has()
:
:has(> tool-tip) {
position: relative;
}
Non preoccuparti troppo del supporto del browser. Innanzitutto, ricorda che questi suggerimenti
sono supplementari. Se non funzionano, non dovrebbero esserci problemi. In secondo luogo, nella sezione
JavaScript implementeremo uno script per il polyfill della funzionalità di cui abbiamo bisogno
per i browser senza supporto :has()
.
Ora rendiamo le descrizioni comando non interattive in modo che non sottraggano eventi puntatore all'elemento principale:
tool-tip {
…
pointer-events: none;
user-select: none;
}
Poi, nascondi la descrizione comando con l'opacità in modo da poterla visualizzare con una dissolvenza incrociata:
tool-tip {
opacity: 0;
}
:has(> tool-tip):is(:hover, :focus-visible, :active) > tool-tip {
opacity: 1;
}
:is()
e :has()
fanno
il lavoro più pesante, rendendo tool-tip
contenente elementi principali consapevole
dell'interattività dell'utente per attivare/disattivare la visibilità di una descrizione comando secondaria. Gli utenti del mouse
possono passare il mouse sopra, gli utenti della tastiera e dello screen reader possono attivare e gli utenti del tocco possono toccare.
Ora che la visualizzazione e l'occultamento della sovrapposizione funzionano per gli utenti vedenti, è il momento di aggiungere alcuni stili per la definizione dei temi, il posizionamento e l'aggiunta della forma triangolare alla bolla. Gli stili seguenti iniziano a utilizzare proprietà personalizzate, basandosi su quanto fatto finora, ma aggiungendo anche ombre, tipografia e colori in modo che assomigli a 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;
}
Modifiche al 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à personalizzate e lasciare che il tema gestisca il 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 molto meno intense regolandone l'opacità.
Da destra a sinistra
Per supportare le modalità di lettura da destra a sinistra, una proprietà personalizzata memorizzerà il valore della direzione del documento in un valore di -1 o 1 rispettivamente.
tool-tip {
--isRTL: -1;
}
tool-tip:dir(rtl) {
--isRTL: 1;
}
Può essere utilizzato per facilitare il posizionamento della descrizione comando:
tool-tip[tip-position="top"]) {
--_x: calc(50% * var(--isRTL));
}
Oltre a indicare la 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 trasformazioni logiche su translateX()
:
--_x: calc(var(--isRTL) * -3px * -1);
Posizionamento della 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
codice seguente mostra lo stile di ciascuna delle quattro posizioni per le direzioni
da sinistra a destra e da destra a sinistra.
Allineamento in alto e all'inizio del 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 alla fine della riga
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 della riga
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 solo attivato/disattivato la visibilità della descrizione comando. In questa sezione, animeremo innanzitutto l'opacità per tutti gli utenti, in quanto si tratta di una transizione con movimento ridotto generalmente sicura. Poi animeremo la posizione di trasformazione in modo che il suggerimento venga visualizzato scorrendo dall'elemento principale.
Una transizione predefinita sicura e significativa
Applica uno stile all'elemento della descrizione comando per la transizione di opacità e trasformazione, come segue:
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ò apparire una descrizione comando, se l'utente accetta il movimento, posiziona leggermente la proprietà translateX dandole una piccola distanza da percorrere:
@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 questo imposta lo stato "out", poiché lo stato "in" è a translateX(0)
.
JavaScript
A mio parere, JavaScript è facoltativo. Questo perché nessuno di questi
suggerimenti dovrebbe essere una lettura obbligatoria per completare un'attività nell'interfaccia utente. Quindi, se i
suggerimenti non funzionano, non dovrebbe essere un problema. Ciò significa anche che possiamo trattare
i suggerimenti come migliorati progressivamente. Alla fine tutti i browser supporteranno
:has()
e questo script potrà essere rimosso completamente.
Lo script polyfill esegue due operazioni e lo fa solo se il browser non supporta :has()
. Innanzitutto, verifica il supporto di :has()
:
if (!CSS.supports('selector(:has(*))')) {
// do work
}
Successivamente, trova gli elementi principali dei <tool-tip>
e assegna loro un nome di classe con cui
lavorare:
if (!CSS.supports('selector(:has(*))')) {
document.querySelectorAll('tool-tip').forEach(tooltip =>
tooltip.parentNode.classList.add('has_tool-tip'))
}
Successivamente, 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)
}
Ecco fatto. Ora tutti i browser mostreranno i suggerimenti se :has()
non è
supportato.
Conclusione
Ora che sai come ho fatto, come faresti tu? 🙂 Non vedo l'ora di utilizzare l'API popup
per semplificare le descrizioni comando, il livello superiore per evitare problemi con l'indice Z e l'API anchor
per posizionare meglio gli elementi nella finestra. Fino ad allora, creerò
descrizioni comando.
Diversifichiamo i nostri approcci e impariamo tutti i modi per creare sul web.
Crea una demo, inviami un tweet con i link e la aggiungerò alla sezione dei remix della community qui sotto.
Remix della community
Ancora nessun elemento da visualizzare.
Risorse
- Codice sorgente su GitHub