Importância da Acessibilidade em Projetos Web
Mais de um bilhão de pessoas no mundo possuem algum tipo de deficiência. Garantir que aplicações web sejam utilizáveis por todos não é apenas uma questão ética, mas também impacta diretamente o alcance e a qualidade do produto. O SvelteKit oferece recursos nativos que facilitam a implementação de acessibilidade (a11y) quando bem aproveitados.
Recursos Nativos do SvelteKit para Acessibilidade
Anúncio de Navegação entre Rotas
O SvelteKit possui um sistema que anuncia mudanças de rota para leitores de tela. Para que isso funcione adequadamente, cada página deve definir um título descritivo e único:
<!-- src/routes/produto/[id]/+page.svelte -->
<svelte:head>
<title>{dadosProduto.nome} - Detalhes do Produto</title>
</svelte:head>
Gerenciamento de Foco
Após a navegação, o foco é redefinido automaticamente. Para personalizar esse comportamento, utilize o hook afterNavigate:
import { afterNavigate } from '$app/navigation';
afterNavigate(() => {
const cabecalho = document.getElementsByTagName('h1')[0];
if (cabecalho) {
cabecalho.setAttribute('tabindex', '-1');
cabecalho.focus();
}
});
Estrutura Semântica de HTML
Utilizar elementos HTML corretos é a base da acessibilidade. Veja as comparações:
| Categoria | Recomendado | Evitar |
|---|---|---|
| Navegação | <nav> |
<div> para menus |
| Títulos | Hierarquia <h1> a <h6> |
Pular níveis de heading |
| Botões interativos | <button> |
<div> com onclick |
| Campos de formulário | <label> associado via for |
Inputs sem label |
<form method="POST" use:enhance>
<label for="campo-email">Endereço de e-mail</label>
<input id="campo-email" name="email" type="email" required>
<label for="campo-senha">Senha de acesso</label>
<input id="campo-senha" name="password" type="password" required>
<button type="submit">Entrar</button>
</form>
Uso de ARIA
<!-- Barra de progresso -->
<div role="progressbar" aria-valuenow={percentual} aria-valuemin="0" aria-valuemax="100">
Carregando: {percentual}%
</div>
<!-- Mensagens de erro -->
<div role="alert" aria-live="assertive">
{#if mensagemErro}
<p>{mensagemErro}</p>
{/if}
</div>
Internacionalização e Suporte a Múltiplos Idiomas
Configuração do Atributo de Idioma
<!-- src/app.html -->
<html lang="%idioma%">
// src/hooks.server.js
export function handle({ event, resolve }) {
const idiomaDetectado = detectarIdioma(event);
return resolve(event, {
transformPageChunk: ({ html }) => html.replace('%idioma%', idiomaDetectado)
});
}
Seletor de Idioma Acessível
<script>
let idiomaAtual = 'pt';
const opcoesIdioma = [
{ codigo: 'pt', nome: 'Português' },
{ codigo: 'en', nome: 'English' },
{ codigo: 'es', nome: 'Español' }
];
</script>
<nav aria-label="Seleção de idioma">
{#each opcoesIdioma as opcao}
<button
aria-pressed={idiomaAtual === opcao.codigo}
on:click={() => idiomaAtual = opcao.codigo}
>
{opcao.nome}
</button>
{/each}
</nav>
Acessibilidade em Formulários
Aprimoramento Progressivo
<script>
import { enhance } from '$app/forms';
let enviando = false;
</script>
<form method="POST" use:enhance={() => {
enviando = true;
return async ({ result }) => {
enviando = false;
// Processar resultado
};
}}>
<button type="submit" disabled={enviando}>
{enviando ? 'Enviando...' : 'Enviar'}
</button>
</form>
Tratamento de Erros no Servidor
// +page.server.js
import { fail } from '@sveltejs/kit';
export const actions = {
default: async ({ request }) => {
const formData = await request.formData();
const emailUsuario = formData.get('email');
if (!emailValido(emailUsuario)) {
return fail(400, {
emailUsuario,
erros: { email: 'Forneça um endereço de e-mail válido' }
});
}
return { sucesso: true };
}
};
Navegação por Teclado
Armadilha de Foco em Modais
<script>
import { onMount, onDestroy } from 'svelte';
let elementoModal;
let elementoAnterior;
onMount(() => {
elementoAnterior = document.activeElement;
elementoModal.focus();
});
onDestroy(() => {
elementoAnterior?.focus();
});
function gerenciarTeclado(e) {
if (e.key === 'Escape') {
fecharModal();
return;
}
if (e.key === 'Tab') {
const focaveis = elementoModal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const primeiro = focaveis[0];
const ultimo = focaveis[focaveis.length - 1];
if (e.shiftKey && document.activeElement === primeiro) {
e.preventDefault();
ultimo.focus();
} else if (!e.shiftKey && document.activeElement === ultimo) {
e.preventDefault();
primeiro.focus();
}
}
}
</script>
<div role="dialog" aria-modal="true" tabindex="-1" bind:this={elementoModal} on:keydown={gerenciarTeclado}>
<!-- Conteúdo do modal -->
</div>
Contraste de Cores e Design Visual
Seguindo as drietrizes WCAG 2.1:
| Tipo de Elemento | Contraste Mínimo | Contraste Recomendado |
|---|---|---|
| Texto normal | 4.5:1 | 7:1 |
| Texto grande | 3:1 | 4.5:1 |
| Componentes de UI | 3:1 | 4.5:1 |
.btn-primario {
background-color: #0d5a8f;
color: #ffffff;
border: 2px solid transparent;
}
.btn-primario:focus-visible {
outline: 3px solid #0d5a8f;
outline-offset: 3px;
}
Testes de Acessibilidade
Testes Automatizados com Playwright
import { test, expect } from '@playwright/test';
test('página inicial deve atender critérios de acessibilidade', async ({ page }) => {
await page.goto('/');
const titulo = await page.title();
expect(titulo).toContain('Bem-vindo');
const atributoIdioma = await page.getAttribute('html', 'lang');
expect(atributoIdioma).toBe('pt');
const todasImagens = await page.$$('img');
for (const img of todasImagens) {
const textoAlternativo = await img.getAttribute('alt');
expect(textoAlternativo).not.toBeNull();
}
});
Lista de Verificação Manual
- Todas as funcionalidades acessíveis via teclado
- Leitores de tela anunciam conteúdo corretamente
- Contraste de cores em conformidade com WCAG
- Mensagens de erro de formulário são claras
- Indicadores de foco visíveis e consistentes
- Títulos de página únicos e descritivos
- Textos de links descritivos sem ambiguidade
- Imagens possuem texto alternativo apropriado
Balanceamento entre Performance e Acessibilidade
Carregamento Sob Demanda
<script>
import { onMount } from 'svelte';
let naTela = false;
let observador;
onMount(() => {
observador = new IntersectionObserver((entradas) => {
naTela = entradas[0].isIntersecting;
});
observador.observe(document.getElementById('secao-deferida'));
});
</script>
<section id="secao-deferida">
{#if naTela}
<h2>Informações Importantes</h2>
<p>Este conteúdo é carregado quando entra na viewport.</p>
{:else}
<div aria-busy="true" role="status">Carregando conteúdo...</div>
{/if}
</section>
Atualizações de Conteúdo Dinâmico
<script>
let contadorNotificacoes = 0;
function dispararNotificacao(texto) {
contadorNotificacoes++;
const areaLive = document.getElementById('area-notificacoes');
if (areaLive) {
areaLive.textContent = texto;
}
}
</script>
<div id="area-notificacoes" aria-live="polite" aria-atomic="true"></div>
Componentes Personalizados Acessíveis
<!-- BotaoCustom.svelte -->
<script>
export let textoAcesso = '';
export let estaDesativado = false;
</script>
<button
{disabled: estaDesativado}
aria-label={textoAcesso}
on:click
class="btn-custom"
>
<slot>{textoAcesso}</slot>
</button>
<style>
.btn-custom:focus-visible {
outline: 2px solid currentColor;
outline-offset: 2px;
}
.btn-custom:disabled {
opacity: 0.55;
cursor: not-allowed;
}
</style>
Monitoramento Contínuo
import { onMount } from 'svelte';
onMount(() => {
window.addEventListener('error', (evento) => {
if (evento.message.includes('ARIA') || evento.message.includes('a11y')) {
console.warn('Problema de acessibilidade detectado:', evento.message);
// Enviar para serviço de monitoramento
}
});
});