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 le etichette o altre informazioni di alto valore. Un utente deve essere in grado di completare completamente la propria attività senza una descrizione comando.
Toggletip e Tooltip
Come per molti componenti, esistono descrizioni diverse di cosa sia 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 comando a pulsante 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 è necessario 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, viene riconosciuta come descrizione comando. Nell'esempio seguente, noterai che il primo elemento link ha un elemento di descrizione comando riconosciuto nella sua struttura ad albero, mentre il secondo no. Il secondo non ha il ruolo. Nella sezione Stile miglioreremo questa visualizzazione ad albero.
A questo punto, la descrizione comando non deve essere 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 aggiungano i contenuti all'elemento principale e, pertanto, non è necessario che l'elemento sia attivo per essere 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 descrizioni comando e non descrizioni pulsanti, 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 l'elemento, il messaggio viene visualizzato.
- 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 la proprietà statica. Ora non resta che testare 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. Lo si può vedere anche durante il debugging nell'albero dell'accessibilità, il testo del link "top" viene eseguito insieme, senza esitazione, con "Guarda, le descrizioni comando!". Lo screen reader non suddivide o 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 con screen reader mette in primo piano il link, viene pronunciata la parola "in alto", poi fa una breve pausa e annuncia "ha tooltip: guarda, tooltip". In questo modo, un utente dello screen reader riceve alcuni utili suggerimenti per l'esperienza utente. L'esitazione crea una bella 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 è sembrata 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 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 implementeremo uno script per eseguire il polyfill della funzionalità di cui abbiamo bisogno per i browser senza supporto di :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.
Ora che la funzionalità di visualizzazione e occultamento dell'overlay funziona per gli utenti vedenti, è il momento di aggiungere alcuni stili per il tema, il posizionamento e la forma a triangolo della bolla. 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 alcuni 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 marcate regolandone l'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;
}
Questo 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 utilizzato anche per le 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/disattivato solo la 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
Personalizza l'elemento della descrizione comando per applicare transizioni di opacità e trasformazione, ad esempio:
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 ciascuno dei lati su cui può essere visualizzata una descrizione comando, se l'utente accetta il movimento, posiziona leggermente la proprietà translateX assegnandole 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 viene impostato lo stato "out", poiché lo stato "in" è translateX(0)
.
JavaScript
Secondo me, il codice JavaScript è facoltativo. Questo perché nessuna di queste scritte di aiuto dovrebbe essere obbligatoria per completare un'attività nell'interfaccia utente. Pertanto, se le ToolTip non funzionano completamente, non dovrebbe essere un problema. Ciò significa anche che possiamo trattare
le descrizioni comando come migliorate 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, controlla se è supportato :has()
:
if (!CSS.supports('selector(:has(*))')) {
// do work
}
Successivamente, individua 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 la 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 l'ho fatto, come lo faresti tu? 🙂 Non vedo l'ora di provare l'popup
API per semplificare i menu a scomparsa, l'top
per evitare conflitti di 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