Una panoramica di base su come creare un elemento personalizzato della descrizione comando adattabile al colore e accessibile.
In questo post voglio condividere la mia opinione su come creare un elemento personalizzato <tool-tip>
adattabile al colore e accessibile. Prova la demo e visualizza il codice sorgente.
Se preferisci i video, ecco una versione di questo post su YouTube:
Panoramica
Una descrizione comando è un overlay non modale, non bloccante e non interattivo contenente informazioni aggiuntive per le interfacce utente. È nascosto per impostazione predefinita e viene visualizzato quando si passa il mouse sopra un elemento associato o lo si seleziona. Non è possibile selezionare una descrizione comando o interagire direttamente con essa. Le descrizioni comando non sostituiscono etichette o altre informazioni di alto valore; un utente deve essere in grado di completare l'attività senza una descrizione comando.
Toggletip e Tooltip
Come molti componenti, esistono descrizioni diverse di una descrizione comando, ad esempio in MDN, WAI ARIA, Sarah Higley e Inclusive Components. Mi piace la separazione tra descrizioni comando e descrizioni pulsanti. Una descrizione comando deve contenere informazioni supplementari non interattive, mentre una descrizione di attivazione/disattivazione può contenere interattività e informazioni importanti. Il motivo principale della distinzione è l'accessibilità, ovvero il modo in cui gli utenti devono accedere al 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'opzione di attivazione/disattivazione del sito Designcember: un overlay con interattività che un utente può bloccare e esplorare, quindi chiudere con la dismissione rapida o il tasto Esc:
Questa sfida 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 necessariamente trasformare gli elementi personalizzati in componenti web. Il browser tratterà
<foo-bar>
come un <div>
. Potresti pensare a un elemento personalizzato come un
nomeclasse con meno specificità. Non è richiesto JavaScript.
<tool-tip>A tooltip</tool-tip>
È come un elemento div con del testo al suo interno. Possiamo collegarci alla struttura ad albero dell'accessibilità
degli screen reader idonei aggiungendo [role="tooltip"]
.
<tool-tip role="tooltip">A tooltip</tool-tip>
Ora per gli screen reader è riconosciuto come descrizione comando. Nell'esempio seguente, noti come il primo elemento link ha un elemento di descrizione comando riconosciuto nella sua struttura e il secondo no? Il secondo non ha il ruolo. Nella sezione Stili miglioreremo la visualizzazione ad albero.
Ora dobbiamo fare in modo che la descrizione comando non sia attivabile. Se uno screen reader non comprende il ruolo della descrizione comando, consente agli utenti di mettere in primo piano <tool-tip>
per leggere i contenuti, ma l'esperienza utente non ha bisogno di questo. Gli screen reader
aggiungeranno il contenuto all'elemento principale e, in questo modo, non richiedono 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 di schede:
<tool-tip inert role="tooltip">A tooltip</tool-tip>
Ho quindi scelto di utilizzare gli attributi come interfaccia per specificare la posizione della
barra di suggerimento. Per impostazione predefinita, tutti i <tool-tip>
assumeranno una posizione "in alto", ma la posizione può essere personalizzata in un elemento aggiungendo tip-position
:
<tool-tip role="tooltip" tip-position="right ">A tooltip</tool-tip>
Per questo tipo di situazioni tendo a utilizzare gli attributi anziché le classi, in modo che al <tool-tip>
non vengano assegnate più posizioni 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 le descrizioni comando e non i suggerimenti di attivazione/disattivazione, questa sezione è molto più semplice. Per prima cosa, vorrei delineare la nostra esperienza utente ideale:
- In spazi limitati o interfacce disordinate, nascondi i messaggi supplementari.
- Quando un utente passa il mouse sopra un elemento, lo mette in primo piano o utilizza il tocco per interagire con un elemento, mostra 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 il movimento ridotto.
Il nostro obiettivo è fornire messaggistica supplementare on demand. Un utente vedente che utilizza il mouse o la tastiera può passare il mouse sopra il messaggio per visualizzarlo e leggerlo con gli occhi. Un utente non vedente che utilizza uno screen reader può mettere a fuoco il messaggio per visualizzarlo e riceverlo tramite lo strumento.
Nella sezione precedente abbiamo trattato l'albero di accessibilità, il ruolo della descrizione comando e il valore inattivo. Non resta che testarlo e verificare che l'esperienza utente riveli in modo appropriato il messaggio della descrizione comando all'utente. Dopo 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" viene eseguito insieme, senza esitazioni, con "Look, tooltips!". Lo screen reader non suddivide né identifica il testo come contenuto della descrizione comando.
Aggiungi un pseudo-elemento solo per screen reader a <tool-tip>
e possiamo aggiungere il nostro testo di 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 di accessibilità aggiornato, che ora contiene un punto e virgola dopo il testo del link e una richiesta per la descrizione comando "Ha descrizione comando: ".
Ora, quando un utente di screen reader attiva il link, dice "in alto" e fa una piccola pausa, poi annuncia "ha descrizione comando: guarda, descrizioni comando". In questo modo, l'utente di uno screen reader offre un paio di suggerimenti utili per l'esperienza utente. L'esitazione crea una buona separazione tra il testo del link e la descrizione comando. Inoltre, quando viene annunciato "ha descrizione comando", un utente dello screen reader può annullarlo facilmente se lo ha già sentito. È molto simile al passaggio del mouse sopra un elemento e al successivo allontanamento, come hai già visto nel messaggio aggiuntivo. Mi sembrava una buona parità di UX.
Stili
L'elemento <tool-tip>
sarà un elemento secondario dell'elemento per cui rappresenta
il messaggio aggiuntivo, quindi iniziamo con gli elementi essenziali per
l'effetto di overlay. Rimuovilo dal flusso di documenti con position absolute
:
tool-tip {
position: absolute;
z-index: 1;
}
Se il contesto principale non è un contesto di impilamento, la descrizione comando si posizionerà in base al contesto di impilamento più vicino, il che non è ciò che vogliamo. Nel
blocco è disponibile un nuovo selettore che può essere utile, :has()
:
:has(> tool-tip) {
position: relative;
}
Non preoccuparti troppo del supporto del browser. Innanzitutto, ricorda che queste
descrizioni comando sono supplementari. Se non funzionano, non c'è problema. In secondo luogo, nella
sezione JavaScript eseguiremo il deployment di uno script per eseguire il polyfill della funzionalità necessaria
per i browser che non supportano il formato :has()
.
Ora rendiamo le descrizioni comando non interattive in modo che non rubino gli eventi del cursore all'elemento principale:
tool-tip {
…
pointer-events: none;
user-select: none;
}
Nascondi la descrizione comando con l'opacità in modo da poterla passare con una transizione graduale:
tool-tip {
opacity: 0;
}
:has(> tool-tip):is(:hover, :focus-visible, :active) > tool-tip {
opacity: 1;
}
:is()
e :has()
fanno il lavoro più difficile, rendendo tool-tip
contenente elementi principali consapevoli dell'interattività dell'utente in modo da attivare/disattivare la visibilità di una descrizione comando secondaria. Gli utenti con mouse possono eseguire il passaggio del mouse, gli utenti con tastiera e screen reader possono attivare il riquadro di immissione e gli utenti con tocco possono toccare.
Poiché l'overlay mostra e nascondi visibile agli utenti vedenti, è il momento di aggiungere alcuni stili per i temi, il posizionamento e l'aggiunta della forma triangolare al fumetto. I seguenti stili iniziano a utilizzare proprietà personalizzate, sulla base di quanto fatto finora, ma aggiungendo anche ombre, tipografia e colori in modo che assomiglino a una descrizione comando fluttuante:
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;
}
Regolazioni 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à 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 regolando la loro opacità.
Da destra a sinistra
Per supportare le modalità di lettura da destra a sinistra, una proprietà personalizzata memorizza 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 facilitare il posizionamento della descrizione comando:
tool-tip[tip-position="top"]) {
--_x: calc(50% * var(--isRTL));
}
Inoltre, ti aiuta a capire dove si trova il 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 utilizzata 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 delle descrizioni comando fisiche e logiche. Il
codice seguente mostra come viene impostato lo stile per ciascuna delle quattro posizioni sia per le direzioni da sinistra a destra sia 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 all'interno
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 a fine 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 l'opzione di visibilità della descrizione comando. In questa sezione, animaeremo inizialmente l'opacità per tutti gli utenti, poiché si tratta di una transizione con movimento ridotto generalmente sicura. Poi animeremo la posizione della trasformazione in modo che la descrizione comando scorra dall'elemento principale.
Una transizione predefinita sicura e significativa
Assegna uno stile all'elemento della descrizione comando per impostare l'opacità della transizione e la trasformazione, 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;
}
Aggiunta di 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à translateX assegnandole una piccola distanza da cui spostare:
@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 viene impostato lo stato "out", poiché lo stato "in" è translateX(0)
.
JavaScript
Secondo me, il codice JavaScript è facoltativo. Il motivo è che nessuna di queste descrizioni comando deve essere letta per svolgere un'attività nella UI. Pertanto, se le ToolTip non funzionano completamente, non dovrebbe essere un problema. Ciò significa anche che possiamo trattare
le descrizioni comando come progressivamente migliorate. 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, controlla se è supportato :has()
:
if (!CSS.supports('selector(:has(*))')) {
// do work
}
Quindi, trova gli elementi principali di <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 della classe, simulando il selettore :has()
per ottenere 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 l'ho fatto, come lo faresti tu? 🙂 Non vedo l'ora di provare l'popup
API per semplificare i toggletip, l'top per non dover più combattere con gli z-index e l'anchor
API per posizionare meglio gli elementi nella finestra. Fino ad allora, creerò le ToolTip.
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.
Remix della community
Ancora nessun elemento da visualizzare.
Risorse
- Codice sorgente su GitHub