Navegação por Teclado
Permita que todos os usuários naveguem eficientemente sem mouse
Atalhos de teclado
- Tab
- Navegar entre elementos interativos
- Shift + Tab
- Navegação para trás
- Entrée / Espace
- Ativar botões e links
- Échap
- Fechar modais e menus
- ↑ ↓
- Navegar em listas e menus
Navegação por teclado em listas
❌ Exemplo ruim
Menu apenas com navegação por Tab:
<!-- Ruim: Apenas navegação por Tab -->
<div class="menu">
<div tabindex="0">🏠 Início</div>
<div tabindex="0">📋 Projetos</div>
<div tabindex="0">👥 Equipe</div>
<div tabindex="0">📊 Estatísticas</div>
<div tabindex="0">⚙️ Configurações</div>
</div>
<!-- Usuário deve pressionar Tab 5 vezes -->
<!-- Sem navegação por setas -->
<!-- Sem semântica de menu -->✅ Exemplo bom
Menu com navegação por setas:
💡 Use ↑ ↓ para navegar, Enter para selecionar
<!-- Bom: navegação por setas com roving tabindex -->
<div
role="menu"
@keydown="handleKeydown"
>
<div
v-for="(item, i) in items"
role="menuitem"
:tabindex="selected === i ? 0 : -1"
@focus="selected = i"
:class="{ selected: selected === i }"
>
{{ item }}
</div>
</div>// Navegação por setas com roving tabindex
function handleKeydown(e) {
switch(e.key) {
case 'ArrowDown':
selected = (selected + 1) % items.length
break
case 'ArrowUp':
selected = selected === 0
? items.length - 1
: selected - 1
break
case 'Enter':
selectItem(selected)
break
}
}Explicação: Listas navegáveis por teclado permitem usar as setas para mover entre elementos, além da navegação clássica por Tab.
Estilos de foco visíveis
❌ Exemplo ruim
Tente navegar com Tab nesta área:
/* ❌ Remove todos os indicadores de foco */
*:focus {
outline: none !important;
}
button:focus,
a:focus,
input:focus {
outline: none !important;
}<!-- ❌ Problemas de acessibilidade: -->
<button>Botão 1</button> <!-- Sem tipo, foco invisível -->
<button>Botão 2</button> <!-- Sem tipo, foco invisível -->
<a href="#">Link</a> <!-- Foco invisível -->
<input type="text" /> <!-- Foco invisível -->
<!-- Navegação por teclado impossível de acompanhar visualmente -->✅ Exemplo bom
Tente navegar com Tab nesta área:
/* ✅ Indicadores de foco visíveis e acessíveis */
button:focus-visible {
outline: 3px solid #4F46E5;
outline-offset: 2px;
}
input:focus-visible {
border-color: #4F46E5;
outline: 2px solid #4F46E5;
outline-offset: 1px;
}<button type="button">Botão 1</button>
<button type="button">Botão 2</button>
<a href="#">Link para página</a>
<input type="text" placeholder="Digite o texto" />Explicação: Usuários navegando por teclado precisam de indicadores visuais claros para saber onde estão na página.
Link de salto (Pular para conteúdo)
❌ Exemplo ruim
Conteúdo principal
O usuário deve navegar por todos os links de navegação...
<!-- Ruim: sem link de salto -->
<header>
<nav>
<a href="#">Início</a>
<a href="#">Produtos</a>
<a href="#">Serviços</a>
<!-- Mais de 10 links de navegação... -->
</nav>
</header>
<main>
<!-- Usuário deve atravessar toda a navegação -->
<h1>Conteúdo principal</h1>
</main>✅ Exemplo bom
Conteúdo principal
O usuário pode acessar diretamente aqui graças ao link de salto!
/* Link de salto oculto por padrão, visível ao focar */
.skip-link {
position: absolute;
top: -100px;
left: 1rem;
z-index: 1000;
padding: 0.5rem 1rem;
background: var(--color-primary);
color: white;
text-decoration: none;
border-radius: 0.625rem;
}
.skip-link:focus {
top: 1rem;
}<a href="#main-content" class="skip-link">
Pular para conteúdo principal
</a>
<header>
<nav aria-label="Navegação principal">
<ul>
<li><a href="/">Início</a></li>
<li><a href="/produits">Produtos</a></li>
<li><a href="/services">Serviços</a></li>
</ul>
</nav>
</header>
<main id="main-content">
<h1>Conteúdo principal</h1>
</main>Explicação: Links de salto permitem que usuários de teclado pulem diretamente para o conteúdo principal sem ter que passar por toda a navegação.
Modal com armadilha de foco
❌ Exemplo ruim
<!-- Ruim: modal sem gerenciamento de foco -->
<div v-if="modalOpen" class="modal-overlay" @click="close">
<div class="modal-content" @click.stop>
<div class="modal-header">
<h3>Confirmar ação</h3>
<button @click="close">×</button>
</div>
<div class="modal-body">
<p>Tem certeza de que deseja continuar?</p>
<input type="text" placeholder="Motivo (opcional)" />
</div>
<div class="modal-footer">
<button @click="close">Cancelar</button>
<button @click="confirm">Confirmar</button>
</div>
</div>
</div>✅ Exemplo bom
<!-- Bom: modal com armadilha de foco -->
<div
v-if="modalOpen"
class="modal-overlay"
@click="closeModal"
@keydown.esc="closeModal"
>
<div
class="modal-content"
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
@click.stop
>
<header class="modal-header">
<h2 id="modal-title">Confirmar ação</h2>
<button
type="button"
@click="closeModal"
aria-label="Fechar modal"
class="close-btn"
>×</button>
</header>
<div class="modal-body">
<p>Tem certeza de que deseja continuar?</p>
<input
ref="firstInput"
type="text"
placeholder="Motivo (opcional)"
@keydown.tab="handleFocusTrap"
/>
</div>
<footer class="modal-footer">
<button
type="button"
@click="closeModal"
@keydown.tab="handleFocusTrap"
>Cancelar</button>
<button
type="button"
ref="lastButton"
@click="confirmAction"
@keydown.tab="handleFocusTrap"
>Confirmar</button>
</footer>
</div>
</div>// Funções essenciais :
// - Foco automático no primeiro elemento
// - Armadilha de foco (Tab/Shift+Tab)
// - Fechar com Escape
// - Retornar foco ao elemento acionador
function handleFocusTrap(event) {
const focusableElements = modalContent.querySelectorAll(
'button, input, [tabindex]:not([tabindex="-1"])'
)
const firstElement = focusableElements[0]
const lastElement = focusableElements[focusableElements.length - 1]
if (event.shiftKey && document.activeElement === firstElement) {
event.preventDefault()
lastElement.focus()
} else if (!event.shiftKey && document.activeElement === lastElement) {
event.preventDefault()
firstElement.focus()
}
}Explicação: Modais devem manter o foco dentro e permitir fechar com a tecla Escape. O foco deve retornar ao elemento que abriu o modal.