Uma visão geral básica de como criar um elemento de dica de ferramenta personalizado acessível e adaptável a cores.
Nesta postagem, quero compartilhar minhas ideias sobre como criar um elemento personalizado <tool-tip> adaptável a cores e acessível. Teste a
demonstração e veja a
fonte.
Se preferir vídeo, confira uma versão desta postagem no YouTube:
Visão geral
Uma dica é uma sobreposição não modal, não bloqueadora e não interativa que contém informações complementares para interfaces de usuário. Ele fica oculto por padrão e é mostrado quando um elemento associado é passado com o cursor ou recebe foco. Não é possível selecionar ou interagir diretamente com uma dica. As dicas não substituem rótulos ou outras informações de alto valor. Um usuário precisa conseguir concluir a tarefa sem uma dica.
Não: confie em dicas em vez de rótulos
Toggletip x dica
Assim como muitos componentes, há várias descrições do que é uma dica, por exemplo, em MDN, WAI ARIA, Sarah Higley e Inclusive Components. Gosto da separação entre dicas de ferramentas e dicas de alternância. Uma dica precisa conter informações complementares não interativas, enquanto uma dica de alternância pode conter interatividade e informações importantes. O principal motivo da divisão é a acessibilidade. Como os usuários devem navegar até o pop-up e ter acesso às informações e aos botões nele? As dicas interativas ficam complexas rapidamente.
Confira um vídeo de uma dica de alternância do site Designcember (link em inglês). Uma sobreposição com interatividade que um usuário pode fixar e explorar e depois fechar com dispensa leve ou a tecla "Esc":
Este desafio de GUI usou uma dica de ferramenta, tentando fazer quase tudo com CSS. Veja como criar uma.
Marcação
Optei por usar um elemento personalizado <tool-tip>. Os autores não precisam transformar elementos personalizados em componentes da Web se não quiserem. O navegador vai tratar
<foo-bar> como um <div>. Pense em um elemento personalizado como um nome de classe com menos especificidade. Não há JavaScript envolvido.
<tool-tip>A tooltip</tool-tip>
É como uma div com algum texto dentro. Podemos adicionar [role="tooltip"] para se conectar à árvore de acessibilidade
de leitores de tela compatíveis.
<tool-tip role="tooltip">A tooltip</tool-tip>
Agora, para leitores de tela, ele é reconhecido como uma dica. Veja no exemplo a seguir como o primeiro elemento de link tem um elemento de dica de ferramenta reconhecido na árvore, e o segundo não. O segundo não tem a função. Na seção de estilos, vamos melhorar essa visualização em árvore.

Em seguida, precisamos que a dica não seja focalizável. Se um leitor de tela não entender a função da dica, ele vai permitir que os usuários foquem o <tool-tip> para ler o conteúdo, e a experiência do usuário não precisa disso. Os leitores de tela
anexarão o conteúdo ao elemento pai e, portanto, não precisam de foco
para serem acessíveis. Aqui, podemos usar inert para garantir que nenhum usuário
encontre acidentalmente o conteúdo da dica no fluxo de guias:
<tool-tip inert role="tooltip">A tooltip</tool-tip>

Depois, escolhi usar atributos como a interface para especificar a posição da
dica. Por padrão, todos os <tool-tip>s assumem uma posição "superior", mas a
posição pode ser personalizada em um elemento adicionando tip-position:
<tool-tip role="tooltip" tip-position="right ">A tooltip</tool-tip>

Eu costumo usar atributos em vez de classes para coisas assim, para que o
<tool-tip> não possa ter várias posições atribuídas a ele ao mesmo tempo.
Pode haver apenas um ou nenhum.
Por fim, coloque elementos <tool-tip> dentro do elemento para o qual você quer fornecer uma
dica. Aqui, compartilho o texto alt com usuários videntes colocando uma imagem e um <tool-tip> dentro de um 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>

Aqui, coloco um <tool-tip> dentro de um
elemento <abbr>:
<p>
The <abbr>HTML <tool-tip role="tooltip" tip-position="top">Hyper Text Markup Language</tool-tip></abbr> abbr element.
</p>

Acessibilidade
Como escolhi criar dicas de ferramentas e não dicas de alternância, esta seção é muito mais simples. Primeiro, vamos definir a experiência do usuário desejada:
- Em espaços limitados ou interfaces desordenadas, oculte as mensagens complementares.
- Quando um usuário passa o cursor, foca ou usa o toque para interagir com um elemento, revele a mensagem.
- Quando o passar o cursor, o foco ou o toque terminar, oculte a mensagem novamente.
- Por fim, reduza qualquer movimento se um usuário tiver especificado uma preferência por movimento reduzido.
Nosso objetivo é oferecer mensagens complementares sob demanda. Um usuário com visão pode passar o cursor do mouse ou do teclado para revelar a mensagem e lê-la com os olhos. Um usuário de leitor de tela sem visão pode focar para revelar a mensagem, recebendo-a por áudio pela ferramenta.
Na seção anterior, abordamos a árvore de acessibilidade, a função da dica e inert. O que resta é testar e verificar se a experiência do usuário revela a mensagem da dica de ferramenta ao usuário. Ao testar, não fica claro qual parte da mensagem audível é uma dica. Ele também pode ser visto durante a depuração na árvore de acessibilidade. O texto do link "top" é executado junto, sem hesitação, com "Look, tooltips!". O leitor de tela não quebra nem identifica o texto como conteúdo de dica.

Adicione um pseudoelemento somente para leitores de tela ao <tool-tip> e inclua seu
próprio texto de solicitação para usuários sem visão.
&::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;
}
Abaixo, você pode conferir a árvore de acessibilidade atualizada, que agora tem um ponto e vírgula após o texto do link e um aviso para a dica "Tem dica: ".

Agora, quando um usuário de leitor de tela foca o link, ele diz "topo", faz uma pequena pausa e anuncia "tem dica: veja, dicas". Isso dá ao usuário do leitor de tela algumas dicas úteis de UX. A hesitação oferece uma boa separação entre o texto do link e a dica. Além disso, quando "tem dica" é anunciado, um usuário de leitor de tela pode cancelar facilmente se já tiver ouvido antes. É muito parecido com passar e tirar o cursor rapidamente, já que você já viu a mensagem complementar. Isso parece uma boa paridade de UX.
Estilos
O elemento <tool-tip> será filho do elemento que representa a mensagem complementar. Portanto, vamos começar com o essencial para o efeito de sobreposição. Remova do fluxo de documentos com o position absolute:
tool-tip {
position: absolute;
z-index: 1;
}
Se o elemento pai não for um contexto de empilhamento, a dica vai se posicionar no mais próximo, o que não é o ideal. Há um novo seletor no
bloco que pode ajudar, :has():
:has(> tool-tip) {
position: relative;
}
Não se preocupe muito com o suporte do navegador. Primeiro, lembre-se de que essas dicas
são complementares. Se eles não funcionarem, não tem problema. Em segundo lugar, na seção
JavaScript, vamos implantar um script para fazer polyfill da funcionalidade necessária
para navegadores sem suporte a :has().
Em seguida, vamos tornar as dicas não interativas para que elas não roubem eventos de ponteiro do elemento pai:
tool-tip {
…
pointer-events: none;
user-select: none;
}
Em seguida, oculte a dica com opacidade para que possamos fazer a transição com um fade cruzado:
tool-tip {
opacity: 0;
}
:has(> tool-tip):is(:hover, :focus-visible, :active) > tool-tip {
opacity: 1;
}
:is() e :has() fazem
o trabalho pesado aqui, fazendo com que tool-tip que contém elementos principais reconheça a
interatividade do usuário para alternar a visibilidade de uma dica de ferramenta secundária. Os usuários de mouse podem passar o cursor, os usuários de teclado e leitor de tela podem focar, e os usuários de toque podem tocar.
Agora que a sobreposição de mostrar e ocultar está funcionando para usuários com visão, é hora de adicionar alguns estilos para temas, posicionamento e adição da forma de triângulo ao balão. Os estilos a seguir começam a usar propriedades personalizadas, aproveitando o que temos até agora, mas também adicionando sombras, tipografia e cores para que pareça uma dica flutuante:

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;
}
Ajustes de tema
A dica só tem algumas cores para gerenciar, já que a cor do texto é herdada da
página pela palavra-chave do sistema CanvasText. Além disso, como criamos propriedades
personalizadas para armazenar os valores, podemos atualizar apenas essas propriedades e
deixar o tema cuidar do resto:
@media (prefers-color-scheme: light) {
tool-tip {
--_bg: white;
--_shadow-alpha: 15%;
}
}

Para o tema claro, adaptamos o plano de fundo para branco e diminuímos a intensidade das sombras ajustando a opacidade.
Da direita para a esquerda
Para oferecer suporte a modos de leitura da direita para a esquerda, uma propriedade personalizada vai armazenar o valor da direção do documento em um valor de -1 ou 1, respectivamente.
tool-tip {
--isRTL: -1;
}
tool-tip:dir(rtl) {
--isRTL: 1;
}
Isso pode ser usado para ajudar a posicionar a dica:
tool-tip[tip-position="top"]) {
--_x: calc(50% * var(--isRTL));
}
Além de ajudar a identificar onde o triângulo está:
tool-tip[tip-position="right"]::after {
--_tip: var(--_left-tip);
}
tool-tip[tip-position="right"]:dir(rtl)::after {
--_tip: var(--_right-tip);
}
Por fim, também pode ser usado para transformações lógicas em translateX():
--_x: calc(var(--isRTL) * -3px * -1);
Posicionamento da dica
Posicione a dica de ferramenta de forma lógica com as propriedades inset-block ou inset-inline para processar as posições física e lógica da dica. O código a seguir mostra como cada uma das quatro posições é estilizada para direções da esquerda para a direita e da direita para a esquerda.
Alinhamento superior e de início do bloco

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;
}
Alinhamento à direita e inline-end

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);
}
Alinhamento inferior e de fim de bloco

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;
}
Alinhamento à esquerda e inline-start

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);
}
Animação
Até agora, só alternamos a visibilidade da dica. Nesta seção, vamos primeiro animar a opacidade para todos os usuários, já que é uma transição de movimento reduzido geralmente segura. Em seguida, vamos animar a posição da transformação para que a dica apareça deslizando para fora do elemento pai.
Uma transição padrão segura e significativa
Estilize o elemento da dica para fazer a transição de opacidade e transformação, assim:
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;
}
Adicionar movimento à transição
Para cada um dos lados em que uma dica pode aparecer, se o usuário não se importar com o movimento, posicione levemente a propriedade translateX, a ela uma pequena distância para percorrer:
@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;
}
}
Observe que isso está definindo o estado "out", já que o estado "in" está em translateX(0).
JavaScript
Na minha opinião, o JavaScript é opcional. Isso porque nenhuma dessas
dicas precisa ser lida para realizar uma tarefa na interface. Portanto, se as
dicas falharem completamente, não será um grande problema. Isso também significa que podemos tratar as dicas como aprimoradas progressivamente. Eventualmente, todos os navegadores vão oferecer suporte a
:has(), e esse script poderá ser totalmente removido.
O script polyfill faz duas coisas, e só faz isso se o navegador não
oferecer suporte a :has(). Primeiro, verifique se há suporte para :has():
if (!CSS.supports('selector(:has(*))')) {
// do work
}
Em seguida, encontre os elementos pai dos <tool-tip>s e atribua a eles um nome de classe para
trabalhar:
if (!CSS.supports('selector(:has(*))')) {
document.querySelectorAll('tool-tip').forEach(tooltip =>
tooltip.parentNode.classList.add('has_tool-tip'))
}
Em seguida, injete um conjunto de estilos que usam esse nome de classe, simulando o seletor :has()
para o mesmo 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)
}
Pronto! Agora todos os navegadores vão mostrar as dicas se :has() não for compatível.
Conclusão
Agora que você sabe como eu fiz isso, como você faria? 🙂 Estou muito ansioso para usar a
API
popup
para facilitar as dicas de alternância, a camada
superior para evitar conflitos de
z-index e a
API
anchor
para posicionar melhor os elementos na janela. Até lá, vou criar dicas.
Vamos diversificar nossas abordagens e aprender todas as maneiras de criar na Web.
Crie uma demonstração, me envie um tweet com o link, e eu vou adicionar à seção de remixes da comunidade abaixo.
Remixes da comunidade
Ainda não há nada aqui.
Recursos
- Código-fonte no GitHub