Creazione di un componente della descrizione comando

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.

Viene mostrata una descrizione comando che funziona in una serie di esempi e combinazioni di colori

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.

Da fare: etichetta sempre gli input.
Cosa non fare: non fare affidamento sulle descrizioni comando anziché sulle etichette

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.

Uno screenshot dell&#39;albero di accessibilità di Chrome DevTools che rappresenta il codice HTML. Mostra un link con il testo &quot;top ; Ha una descrizione comando: Hey, una descrizione comando!&quot; che può essere selezionato. Al suo interno è presente il testo statico &quot;top&quot; e un elemento di descrizione comando.

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>

Un altro screenshot dell&#39;albero di accessibilità di Chrome DevTools, questa volta manca l&#39;elemento della descrizione comando.

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>

Uno screenshot di un link con una descrizione comando a destra che dice &quot;Una descrizione comando&quot;.

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>

Uno screenshot di un&#39;immagine con una descrizione comando che dice &quot;Logo del cranio delle sfide GUI&quot;.

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>

Uno screenshot di un paragrafo con l&#39;acronimo HTML sottolineato e una descrizione comando sopra che dice &quot;Hyper Text Markup Language&quot;.

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:

  1. In spazi limitati o interfacce disordinate, nascondi i messaggi supplementari.
  2. 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.
  3. Quando il passaggio del mouse, lo stato attivo o il tocco termina, nascondi di nuovo il messaggio.
  4. 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.

Screenshot di VoiceOver di macOS che legge un link con una descrizione comando

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.

Uno screenshot dell&#39;albero di accessibilità di Chrome DevTools in cui il testo del link è &quot;In alto Hey, una descrizione comando!&quot;.

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: ".

Uno screenshot aggiornato dell&#39;albero di accessibilità di Chrome DevTools in cui il testo del link ha una formulazione migliorata, &quot;top ; Has tooltip: Hey, a tooltip!&quot;.

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():

Supporto dei browser

  • Chrome: 105.
  • Edge: 105.
  • Firefox: 121.
  • Safari: 15.4.

Origine

: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:

Uno screenshot della descrizione comando in modalità scura, sopra il link &quot;block-start&quot;.

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%;
  }
}

Uno screenshot affiancato delle versioni chiara e scura della descrizione comando.

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

Uno screenshot che mostra la differenza di posizionamento tra la posizione superiore da sinistra a destra e la posizione superiore da destra a sinistra.

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

Uno screenshot che mostra la differenza di posizionamento tra la posizione a destra da sinistra a destra e la posizione in linea finale da destra a sinistra.

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

Uno screenshot che mostra la differenza di posizionamento tra la posizione inferiore da sinistra a destra e la posizione di fine blocco da destra a sinistra.

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

Uno screenshot che mostra la differenza di posizionamento tra la posizione a sinistra da sinistra a destra e la posizione iniziale in linea da destra a sinistra.

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