ADR-0024 Atalhos de teclado
Atalhos de teclado padrão da empresa — TanStack Hotkeys, fixado em versões exatas enquanto for pré-1.0, com regras de WCAG 2.1.4 e de descoberta para cada atalho.
- Decisão
- TanStack Hotkeys, fixadoConstruir a app
- Estado
- Aceite
- Data
- Âmbito
- Predefinição da empresa para todas as novas apps
Ligações
Contexto
Seção intitulada “Contexto”As interfaces densas — tabelas, editores, paletas de comandos, diálogos — pedem atalhos de teclado. Fazê-los bem é sobretudo uma questão de casos-limite: disposições de teclado, Command no macOS versus Control nos restantes, escrever em campos, widgets com foco que usam as mesmas teclas, composição IME, um diálogo e a página por trás a quererem ambos o Escape. Feitos sem cuidado, os atalhos são também um risco de acessibilidade: o WCAG 2.1.4 Character Key Shortcuts (Nível A) exige que um atalho feito apenas de teclas de letra, número, pontuação ou símbolo possa ser desativado, remapeado para incluir um modificador como Control ou Alt, ou esteja ativo apenas enquanto o seu componente tem foco.
As decisões à volta estão tomadas:
- Componentes: shadcn/ui sobre Base UI, que é dono do foco e das teclas dentro de diálogos, menus e selects (ADR-0011)
- Formulários: TanStack Form com submissão nativa de
<form>(ADR-0013) - Renderização: o TanStack Start renderiza no servidor e depois hidrata (ADR-0002)
- Testes:
bun testcom happy-dom para componentes, Playwright para o comportamento do teclado e end-to-end (ADR-0014)
O que está em aberto: que biblioteca regista os atalhos, como os atalhos convivem com o tratamento de teclado do próprio Base UI, e que regras de acessibilidade todos os atalhos têm de cumprir.
Restrições:
- Um atalho nunca é o único caminho. As WAI-ARIA Authoring Practices dizem para não usar atalhos de teclado como substituto do acesso pela navegação normal
- Pré-1.0, e a mover-se depressa. A visão geral do Hotkeys chama-lhe beta, com uma API “still subject to change”; os pacotes são
@tanstack/hotkeys0.17.0 e@tanstack/react-hotkeys0.16.0 (ambos publicados em 2026-10-19). A política é a que o ADR-0008 definiu para o Drizzle v1 enquanto era release candidate: adotar, fixar versões exatas, atualizar de forma deliberada
Opções consideradas
Seção intitulada “Opções consideradas”Biblioteca
Seção intitulada “Biblioteca”1. TanStack Hotkeys
Seção intitulada “1. TanStack Hotkeys”Atalhos de teclado com tipos seguros, com scopes, sequências e remapeamento pelo utilizador.
Prós
- Bindings tipados:
Mod+Spara chords,['G', 'G']para sequências; uma gralha é um erro de tipos. OModresolve para Command no macOS e Control no Windows e no Linux - Predefinições sensatas: as teclas simples são ignoradas enquanto o utilizador escreve num campo de texto, mas os chords com
Control/Metae oEscapecontinuam a disparar. Botões, menus, listboxes, separadores e sliders com foco mantêm as teclas que usam, seguindo as WAI-ARIA Authoring Practices. Disposições de teclado, AltGr e composição IME são tratados - Scopes: um hotkey está ativo enquanto o seu componente está montado; os scopes aninhados ganham; um scope
exclusive— o de um diálogo, por exemplo — suspende todos os outros scopes, incluindo oglobal. Os scopes seguem a árvore de componentes, pelo que os portais funcionam - App hotkeys: declarar cada atalho uma só vez com
defineAppHotkeys, para que diálogos de ajuda e ecrãs de definições listem atalhos de páginas que ainda não carregaram. Os utilizadores podem remapear ou remover atalhos através de um recorder, e os conflitos são verificados em todas as declarações. A biblioteca não guarda nada; a app decide onde - A sua documentação trata o WCAG 2.1.4 diretamente:
enabledpara uma definição do utilizador, app hotkeys para remapear,targetpara só com foco - Não liga listeners durante a renderização no servidor; MIT; intervalo de peer
react >=16.8; o@tanstack/react-hotkeysdepende exatamente do@tanstack/hotkeys0.17.0 e reexporta-o
Contras
- Beta, e a API move-se depressa. Publicado pela primeira vez em 2026-02-10; da 0.11.0 (2026-10-04) à 0.17.0 (2026-10-19) saíram seis minors em 15 dias. A 0.12.0 (2026-10-14) removeu
useHotkeySequenceeuseHotkeySequences, renomeouHotkeyparaHotkeyChordeSequenceManagerparaHotkeyManager; a 0.9.0 (2026-09-21) fez os recorders usar por predefinição códigos de teclas físicas. A documentação pede aos early adopters que ajudem com “edge cases across multiple keyboard layouts, locales, and operating systems” - Jovem: o repositório foi criado em 2026-01, pelo que há muito menos experiência na comunidade do que com as alternativas
- As predefinições tomam conta da tecla premida:
preventDefaultestopPropagationestão ativos, pelo que um atalho registado esconde a ação do browser e outros listeners - A biblioteca oferece as ferramentas de acessibilidade mas não impõe o WCAG 2.1.4 — essa regra é nossa
2. react-hotkeys-hook
Seção intitulada “2. react-hotkeys-hook”5.3.3 (2026-06-26), MIT, mantido ativamente.
Prós: um major estável com um historial muito mais longo; useHotkeys com scopes (HotkeysProvider, toggleScope); hotkeys desligados em campos de formulário por predefinição (enableOnFormTags); sequências (sequenceTimeoutMs, 1000 ms por predefinição); useRecordHotkeys para ecrãs de definições; uma opção description para painéis de ajuda.
Contras: a sua documentação não lista um registo declarado com remapeamento pelo utilizador e verificação de conflitos, nem helpers para cheat sheets — teríamos de os construir. O preventDefault vem desligado por predefinição, pelo que cada Mod+S precisa de o ter definido. É a escolha se o risco de pré-1.0 pesar mais do que essas funcionalidades, e o nosso plano B se o Hotkeys desiludir.
3. tinykeys
Seção intitulada “3. tinykeys”4.0.1 (2026-09-25), MIT, “~1KB” segundo o seu README.
Prós: minúsculo; $mod para o modificador da plataforma; sequências ("g i", timeout de 1000 ms); ignora por predefinição eventos de input, textarea, select e contenteditable; devolve uma função de unsubscribe.
Contras: agnóstico de framework, pelo que escrevemos o useEffect e a limpeza a cada utilização. O seu README não tem scopes, recorder, remapeamento nem deteção de conflitos, e não tem noção de widgets com foco para além das tags de formulário.
4. Mousetrap
Seção intitulada “4. Mousetrap”1.6.5; Apache-2.0 com a exceção LLVM.
Prós: estabelecido há muito — há versões desde pelo menos 2015. Contras: a última versão no npm (1.6.5) é de 2020-01-23 e o último push para o seu repositório é de 2023-03-15. Na prática sem manutenção; excluído só por isso.
Não é uma biblioteca
Seção intitulada “Não é uma biblioteca”A. Um listener keydown escrito à mão
Seção intitulada “A. Um listener keydown escrito à mão”Prós: zero dependências. Contras: a lista de armadilhas do próprio Hotkeys é a lista que iríamos redescobrir — disposições de teclado, sistemas operativos, atalhos personalizados, scopes em conflito, ignorar inputs, repetição de teclas, composição IME e teclas que parecem presas depois de mudar de janela. As cheat sheets e as definições deixam de coincidir com os atalhos reais.
Que atalhos permitimos
Seção intitulada “Que atalhos permitimos”- Chords com modificador e teclas que não são de caracteres por predefinição; atalhos de teclas de caracteres só atrás de um interruptor. Os chords com
Mod, oEscapee as teclas de função estão isentos do WCAG 2.1.4;?,/ouG Hprecisam de uma definição do utilizador. Uma só regra cumpre o critério - Vale tudo. O mais rápido para utilizadores avançados, mas falha o WCAG 2.1.4 e dispara por engano para utilizadores de entrada por voz, cujo ditado chega como teclas premidas
Decisão
Seção intitulada “Decisão”O TanStack Hotkeys, através do @tanstack/react-hotkeys fixado em versões exatas, é o padrão da empresa para atalhos de teclado em todas as novas apps. Todos os atalhos seguem as regras de acessibilidade abaixo.
Em concreto:
- Pacote:
@tanstack/react-hotkeys0.16.0, instalado combun add --exact. Reexporta o core e fixa ele próprio o@tanstack/hotkeys0.17.0, pelo que o core não se instala à parte - Fixar versões exatas (sem
^) enquanto o Hotkeys for pré-1.0; atualizar de forma deliberada, como no ADR-0008. O Renovate (ADR-0016) abre o PR mas nunca o faz merge automático; uma pessoa lê primeiro as notas de lançamento — as da 0.12.0 trazem uma tabela de antes e depois de todas as APIs renomeadas. Reavaliar na 1.0 - Registar no componente que é dono da ação com
useHotkeyouuseHotkeys; o atalho existe enquanto o componente estiver montado. Escrever os bindings comMod, nuncaControl+SnemMeta+S, para que cada plataforma tenha o seu modificador. Tipá-los comoHotkeyChordouHotkey - Todo o atalho que um utilizador vê é um app hotkey. Declará-lo com
defineAppHotkeysnum módulo só de declarações, commeta.nameemeta.group, e passá-lo aoHotkeysProviderda raiz. Os ids são um contrato estável — os hotkeys personalizados guardados são indexados por eles. Teclas locais pontuais, como oEscapenum popup, podem ser umuseHotkeysimples - Os scopes seguem as regiões da UI. Os atalhos da página ficam em
global. Cada diálogo, popover ou paleta envolve os seus hotkeys emHotkeysProvider scope="…" exclusive, com um nome de scope único - O Base UI é dono das suas teclas (ADR-0011):
- Diálogos: o Base UI move o foco para dentro, faz o Tab circular e “Esc requests close”. Fechar com
Escapeatravés doonOpenChangedo diálogo, não com um segundo hotkeyEscape. Os atalhos só do diálogo vivem num scope exclusivo, para que os atalhos da página fiquem suspensos enquanto está aberto - Menus, selects, separadores e outros widgets mantêm setas, Home, End, Enter, Espaço e typeahead. O Hotkeys ignora as teclas que um widget ARIA com foco usa, mas nunca ignora chords com
Control,MetaouAlt. Logo: nenhum atalho de seta,EnterouSpacesimples ao nível do documento; uma região que precise deles recebe um ref detargetpara o seu próprio elemento - Campos de texto: deixar o
ignoreInputsna predefinição, para que as teclas simples nunca disparem enquanto se escreve. OEnteré a submissão nativa do formulário (ADR-0013), nunca um hotkey - Usar
capture: trueapenas com uma razão escrita — corre antes do handler do próprio widget com foco
- Diálogos: o Base UI move o foco para dentro, faz o Tab circular e “Esc requests close”. Fechar com
- Regras de acessibilidade (WCAG 2.1.4, Nível A, e as WAI-ARIA Authoring Practices):
- Os atalhos de teclas de caracteres precisam de um interruptor. Qualquer atalho feito de uma tecla de letra, número, pontuação ou símbolo — sozinha, com
Shiftou numa sequência — tem uma de: uma definição do utilizador que o desativa (enabled), remapeamento pelo utilizador (app hotkeys com o recorder) ou umtargetpara só funcionar com foco. A predefinição é uma só definição “Atalhos de tecla única” que os controla a todos. Os chords comMod,ControlouAlt, e teclas comoEscapee as de função, estão isentos - Nunca o único caminho. Todas as ações continuam acessíveis pela ordem normal de foco e por controlos visíveis
- Evitar conflitos de teclas com o sistema operativo, o browser e as tecnologias de apoio. Nenhum modificador com
Tab,Enter,SpaceouEscape; nenhumMetacom uma só outra tecla; nenhumAltcom uma tecla de função; nada construído sobreCaps Lock,InsertouScroll Lock(os leitores de ecrã usam-nas como modificadores); no macOS, nenhuma combinaçãoControl+Option; ter em atenção a barra de endereços do browser, o refresh, os marcadores e o histórico, e a pesquisa na página. Os browsers ficam comMod+T,Mod+NeMod+Wde qualquer forma. Um conflito intencional, comoMod+Snum editor, só é permitido para uma função da app muito usada quando a função do browser raramente é usada e tem outro caminho — o teste das próprias Practices - Descobrível. Mostrar o atalho no próprio controlo —
<kbd>(oKbddo shadcn/ui) comformatForDisplaydo mesmo binding que está registado — e ter um diálogo de ajuda construído a partir deuseAppHotkeyListe aberto a partir de um controlo visível. O atalho do diálogo de ajuda é um extra, nunca a única entrada - Ecrã de definições: o interruptor de tecla única, mais remapeamento com
useHotkeyRecorder,findConflictseuseCustomHotkeys. Onde se guardam os hotkeys personalizados é uma escolha do produto, mas têm de estar carregados antes de o provider montar: ele lêcustomHotkeysuma só vez
- Os atalhos de teclas de caracteres precisam de um interruptor. Qualquer atalho feito de uma tecla de letra, número, pontuação ou símbolo — sozinha, com
- Renderização no servidor: o Hotkeys não liga listeners no servidor, e o
Modé detetado a partir denavigator— o servidor não sabe qual é a plataforma do utilizador. Renderizar as etiquetas dos atalhos depois da hidratação (useHydrated()de@tanstack/react-router, como no ADR-0013), não no HTML do servidor, e registar só através de hooks, nunca ao nível do módulo - Testes (ADR-0014): o Playwright prime
ControlOrMeta+Ke semelhantes, pelo que um só teste cobre ambas as plataformas. Cada produto testa que desligar os atalhos de tecla única os desativa, e que um diálogo aberto suspende os atalhos da página - SOLID: os hooks são donos do comportamento; os componentes ficam apresentacionais. Um botão ou item de menu recebe
onSelecte uma etiquetashortcut, e renderiza ambos. A página ou o hook da funcionalidade regista o hotkey e chama o mesmo handler que o clique usa — um handler, dois gatilhos. O módulo de declarações é a fonte única para o registo, o diálogo de ajuda e o ecrã de definições. Não há wrapper à volta douseHotkey: a API da biblioteca já é a abstração, e um wrapper limitar-se-ia a copiar a sua superfície. Reavaliar se as atualizações começarem a tocar em muitos ficheiros
Os fatores decisivos: os casos-limite de disposição de teclado, campos de entrada, widgets com foco e scopes tratados por nós; um registo declarado que alimenta atalhos, ajuda e remapeamento; e a sua documentação a tratar o WCAG 2.1.4. Aceitamos o risco de pré-1.0, que é superior ao do Pacer, e uma comunidade mais pequena.
Consequências
Seção intitulada “Consequências”Mais fácil:
- Bindings tipados;
Mode as disposições de teclado funcionam entre plataformas sem código por sistema operativo - Escrever em campos, usar menus e listboxes e empilhar diálogos não dispara atalhos da página por engano
- Uma só declaração alimenta o atalho, a sua etiqueta no ecrã, o diálogo de ajuda e o ecrã de remapeamento, pelo que não conseguem divergir
- O WCAG 2.1.4 passa a ser uma definição e um teste por produto, e não uma discussão por atalho
Mais difícil:
- Risco de pré-1.0, dito às claras. A documentação do Hotkeys chama-lhe beta, e ele move-se depressa: seis minors em 15 dias, com renomeações que quebram a API na 0.12.0 (2026-10-14) e na 0.9.0 (2026-09-21). Versões fixadas significam sem correções automáticas: cada atualização é um PR deliberado, verificado contra as notas de lançamento, e os tutoriais mais antigos, e as ferramentas de IA, vão mostrar APIs que já não existem (
useHotkeySequence) - Gatilhos de reavaliação: o Hotkeys chega à 1.0 (planear a janela de atualização e rever a política de fixação); uma atualização quebra algo que não conseguimos corrigir dentro do próprio PR de atualização; ou o projeto deixa de lançar versões. A saída custa mais do que a do Pacer: sair significa reconstruir app hotkeys, scopes e remapeamento sobre o
react-hotkeys-hook(5.x estável) ou otinykeys. É por isso que as declarações vivem num só módulo - A acessibilidade é regra nossa, não da biblioteca. Rever à procura de atalhos de tecla simples sem o interruptor, e de ações acessíveis só por atalho
- O Hotkeys decide o que um widget com foco mantém pelo papel ARIA. Não verificámos cada componente do Base UI contra essa tabela, pelo que se testa antes de lançar um atalho de tecla simples numa página com menus ou listboxes
- Os ids dos app hotkeys são um contrato: renomear um deixa cair o hotkey personalizado guardado pelos utilizadores para esse id, e o Hotkeys ignora ids desconhecidos com um aviso
- Os atalhos do browser e do sistema operativo não se podem sobrepor; algumas combinações desejadas vão estar ocupadas
- As etiquetas dos atalhos dependem da plataforma do utilizador; impressas no HTML do servidor podem discordar do cliente, pelo que se renderizam depois da hidratação
Do que abdicamos: do major estável e do historial do react-hotkeys-hook, da dimensão mínima do tinykeys e da simplicidade sem dependências de um listener escrito à mão. Aceite — um registo declarado com scopes, remapeamento e orientação de acessibilidade justifica uma dependência fixada em pré-1.0 e uma atualização planeada na 1.0.

