Teclado e acessibilidade
Tudo é operável sem mouse, e as teclas são as do elemento nativo e do ARIA APG — quem já sabe usar um
<select> não precisa aprender nada. Esta página junta num lugar só o que cada componente
faz com o teclado e o que ele anuncia a quem usa leitor de tela.
Tab não abre painel
Chegar num campo pelo Tab nunca abre calendário, lista ou seletor de cor. Abrir é sempre
explícito: ↓, clique, e no select também Enter e Espaço.
Abrir no foco parecia conveniente e era a origem de dois defeitos. Os painéis se empilhavam ao tabular — quem
atravessa um formulário até o botão de salvar levava um calendário na cara a cada campo, com o painel cobrindo
o campo seguinte. E no campo de cor o painel cobria o próprio campo de quem queria digitar o hex. É a regra do
<select> nativo, e é a do ARIA APG.
| Componente | Abre com |
|---|---|
| Date picker | ↓, clique; Espaço com o campo vazio |
| Select | ↓, ↑, Enter, Espaço, clique |
| Color picker | ↓ no campo ou na amostra; Enter e Espaço na amostra, que é um <button> |
| Menu suspenso | ↓, ↑, Enter, Espaço ou clique no gatilho |
No campo de data, o Enter é do formulário
O date picker é um campo de texto dentro de um <form>, e Enter num campo de texto envia o formulário — é o que qualquer pessoa espera depois de digitar a data. Sequestrar a tecla quebraria o envio em silêncio em todo formulário que já existe, então ali quem abre é o Espaço. E só com o campo vazio: em modo com hora se digita 07/09/2026 14:30, e o espaço é digitação. Com o painel aberto, o Enter confirma o que foi digitado.
Qualquer painel que abriu fecha quando o foco sai dele, então tabular pela página nunca deixa menus abertos para trás.
Date picker
| Tecla | Onde | Ação |
|---|---|---|
↓ | Campo, fechado | Abre o calendário |
Espaço | Campo vazio, fechado | Abre o calendário |
Enter | Campo, aberto | Confirma a data digitada e fecha; fechado, envia o formulário |
← → | Dia | Dia anterior e seguinte |
↑ ↓ | Dia | Mesma data na semana anterior e na seguinte |
PageUp PageDown | Dia | Mês anterior e seguinte; com Shift, ano |
Home End | Dia | Início e fim da semana, respeitando o primeiro dia do locale |
Enter Espaço | Dia | Escolhe o dia |
↑ ↓ | Coluna de hora | Valor anterior e seguinte da coluna |
Home End | Coluna de hora | Primeiro e último valor da coluna |
Enter Espaço | Coluna de hora | Escolhe o valor |
Tab | Painel | Circula dentro do painel, sem escapar para a página; cada coluna de hora é uma parada só |
Esc | Painel | Fecha, descarta o que não foi confirmado e devolve o foco ao campo |
O campo é um combobox com aria-haspopup="dialog" e aria-expanded, e ganha
aria-controls apontando para o painel enquanto ele está aberto. O painel é role="dialog"
com o rótulo "Selecionar data" ou "Selecionar período"; cada mês é um role="grid" com o nome do mês,
cada semana um row e cada dia um gridcell com aria-selected e o rótulo por extenso — "sábado, 12 de
setembro de 2026". Dia fora dos limites leva aria-disabled: recebe foco, mas não é escolhido. A troca de mês é
anunciada por uma região aria-live="polite" que fica fixa no painel. Cada coluna de hora, minuto e segundo é um
grupo de botões com uma parada de Tab só, e o valor escolhido leva aria-pressed. Fechar por clique fora não devolve o foco ao campo, para não
roubá-lo de onde a pessoa clicou.
Select
| Tecla | Ação |
|---|---|
↓ ↑ Enter Espaço | Fechado, abre o painel |
↑ ↓ | Aberto, anda entre as opções, pulando as desativadas e dando a volta |
Home End | Primeira e última opção, pulando as desativadas |
Enter | Escolhe a opção ativa; no múltiplo, marca ou desmarca |
| Letras | Com busca ligada, filtram a lista ignorando acentos. A busca liga sozinha a partir de 6 opções, ou com data-search="true" |
Backspace | No múltiplo, com a busca vazia, remove a última tag |
Backspace Delete | No simples, com a busca vazia, limpa o valor, como o X — sem o X (clearable: false), não limpa |
Esc | Fecha e devolve o foco ao campo |
Com o <select> nativo disabled, o campo sai do caminho do Tab, fica
esmaecido, não abre, não aceita texto e esconde o X de limpar e o das tags. Opção dentro de
<optgroup disabled> também conta como desativada.
O controle é um combobox com aria-haspopup="listbox", aria-expanded e
aria-controls apontando para a lista enquanto ela está aberta — fechada, a lista sai do DOM e o atributo
sai junto. A lista é listbox, com
aria-multiselectable no múltiplo; cada opção é option com aria-selected e
aria-disabled. A opção ativa chega ao leitor de tela por aria-activedescendant, sem tirar
o foco do campo de busca. O X de cada tag se chama "Remover" seguido do nome da opção. O
<select> nativo continua no formulário com aria-hidden, para não ser lido duas
vezes.
Color picker
| Tecla | Onde | Ação |
|---|---|---|
↓ | Campo ou amostra | Abre o painel com o foco na área de cor |
Enter Espaço | Amostra | Abre o painel com o foco na área de cor, e fecha |
← → | Área de cor | Saturação, de 2 em 2%; com Shift, de 10 em 10 |
↑ ↓ | Área de cor | Brilho, no mesmo passo |
← ↓ / → ↑ | Barras de matiz e opacidade | Diminui e aumenta de 1 em 1; com Shift, de 10 em 10 |
Home End | Barras de matiz e opacidade | Mínimo e máximo |
Esc | Painel | Fecha e devolve o foco à amostra |
A amostra é um <button> "Escolher cor" com aria-haspopup="dialog" e
aria-expanded; o painel é role="dialog" "Seletor de cor". As barras são
role="slider" com aria-valuenow, e cada cor da paleta é um botão com o próprio valor como
rótulo. O campo de texto continua sendo o <input> onde se digita o hex.
Menu suspenso
| Tecla | Onde | Ação |
|---|---|---|
↓ | Gatilho | Abre e entra no primeiro item |
↑ | Gatilho | Abre e entra no último item |
Enter Espaço | Gatilho | Abre e fecha |
↑ ↓ | Menu | Item anterior e seguinte, dando a volta e pulando os desativados |
Home End | Menu | Primeiro e último item |
Enter | Item | Aciona; item sem href é <button> e aceita também Espaço |
Esc | Menu | Fecha e devolve o foco ao gatilho |
Tab | Menu | Fecha e segue para o próximo elemento da página |
O gatilho ganha aria-haspopup="menu" e aria-expanded; o painel é role="menu", cada
item menuitem, o separador separator e o item desativado aria-disabled. Os itens
têm tabindex="-1" de propósito: dentro de um menu quem navega é a seta, e itens tabuláveis fariam o
Tab sair do menu item a item.
Abas
| Tecla | Ação |
|---|---|
Tab | Entra na aba aberta e sai da lista direto para o painel — a lista é uma parada só |
← → | Aba anterior e seguinte, dando a volta e pulando as desativadas |
Home End | Primeira e última aba |
Enter Espaço | Com data-manual="true", troca o painel; sem ele, a seta já troca |
O script põe tablist, tab e tabpanel, liga cada aba ao painel por
aria-controls e aria-labelledby, e mantém aria-selected e o
tabindex em dia. O modo manual existe para painel que carrega por HTMX: com ativação automática,
atravessar quatro abas com a seta dispararia quatro requisições.
Modal e gaveta
| Tecla | Ação |
|---|---|
Tab Shift+Tab | Circulam dentro do diálogo, sem escapar para a página |
Esc | Fecha com animação e devolve o foco a quem abriu — a não ser com closable: false ou data-closable="false" |
Os dois são <dialog> aberto com showModal(), e é daí que vem a acessibilidade: a
página atrás fica inerte, o foco fica preso e volta ao gatilho no fechamento, tudo pelo navegador e não por
JavaScript nosso. O Escape nativo fecharia na hora, sem animação; a biblioteca intercepta o
cancel para fechar pelo mesmo caminho do X. Com título, o diálogo é rotulado por ele via
aria-labelledby, e o X se chama "Fechar". No Tucano.confirm(), fechar pelo
Esc é uma recusa: a promessa resolve false.
Tooltip
| Tecla ou gesto | Ação |
|---|---|
Tab até o elemento | Mostra a dica na hora, sem o atraso do ponteiro |
Tab para fora | Esconde a dica |
Esc | Esconde, mesmo com o ponteiro parado em cima |
| Toque | Em tela de toque, tocar abre e tocar de novo fecha |
O balão é role="tooltip" e o elemento aponta para ele por aria-describedby, então o leitor
de tela lê a dica junto com o nome do botão. Um elemento que não recebe foco — um <span>, por
exemplo — ganha tabindex="0", senão quem usa teclado nunca veria a dica. O Esc com o
ponteiro em cima é o que a WCAG 1.4.13 pede, para quem usa ampliação de tela e precisa tirar da frente uma dica
que cobre o conteúdo.
Tabela
| Tecla | Ação |
|---|---|
Tab | Passa pelos cabeçalhos ordenáveis, pelas caixas de seleção e pelos botões das linhas |
Enter | No cabeçalho, ordena — no modo servidor, segue o link |
Espaço | Marca a caixa; no modo cliente, também ordena pelo botão do cabeçalho |
O cabeçalho ordenado tem aria-sort="ascending" ou "descending" e os outros
"none". No modo servidor o gatilho é um <a>, anunciado como link; no cliente, um
<button>. A caixa do cabeçalho é "Selecionar todas as linhas desta página", a de cada linha
"Selecionar linha", e o estado misto é o indeterminate nativo.
Os outros componentes
Estes não têm teclado próprio: o que responde é o elemento nativo por baixo. Mesmo assim, cada um tem algo a dizer ao leitor de tela.
| Componente | Teclado | O que anuncia |
|---|---|---|
| Acordeão | Enter e Espaço no <summary>, do navegador | Aberto e fechado, pelo <details>; a seta é aria-hidden |
| Upload | Enter e Espaço na zona abrem a janela de arquivos | A zona é role="button" descrita pela dica de tipos e tamanho; arquivo recusado aparece num aviso role="alert" |
| Máscara com olho | Enter e Espaço no botão do olho | "Mostrar" e "Ocultar", com aria-pressed; com data-validate, o campo reprovado ganha aria-invalid |
| Editor de texto | Ctrl/⌘ + B, I, U, K; Tab numa tabela anda de célula e, na última, cria uma linha | A área é role="textbox" multilinha; as barras são role="toolbar" e os botões de formatação têm aria-pressed |
| Toast | O relógio pausa quando algo dentro dele recebe foco | Duas regiões ao vivo criadas antes da primeira mensagem: polite para o normal e assertive para erro; cada toast é status ou alert |
| Paginação | Links nativos | <nav> "Paginação", aria-current="page" na atual; a ponta desativada é <span>, fora do caminho do Tab |
| Caixa, opção e chave | As do <input> nativo | As do navegador; a chave precisa de role="switch" no template, porque CSS não põe papel |
| Grupo de botões | As do <input> nativo: o grupo de rádios é uma parada só do Tab, e as setas andam por ele | Precisa de nome no grupo — role="group" com aria-label, ou <fieldset> com <legend>; o <input> fica invisível, mas focável e anunciado |
O que vale para todos
As decisões que se repetem de componente em componente.
| Regra | Por quê |
|---|---|
| O elemento nativo continua no formulário | Nome, obrigatoriedade, estado misto e o anúncio do leitor de tela continuam sendo os do navegador |
Campo com erro é aria-invalid="true", e não classe | É o atributo que o leitor de tela anuncia, e o Django 5 o escreve sozinho. Nos componentes que trocam o campo por um controle próprio, o atributo fica no nativo e o CSS alcança o controle |
| Painel que abriu fecha quando o foco sai | Sem isso, tabular pela página deixava um rastro de painéis abertos |
Esc fecha e devolve o foco a quem abriu | Fechar largando o foco no começo da página faria quem usa teclado recomeçar do topo |
Desenho vai com aria-hidden | Setas, ícones e reticências da paginação não são lidos em voz alta |
prefers-reduced-motion desliga as animações | Vale para painéis, diálogos, toasts, abas, acordeão, tabela e botões |