Guia de Acessibilidade em Aplicações SvelteKit

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
        }
    });
});

Tags: SvelteKit Acessibilidade WCAG ARIA a11y

Publicado em 9-4 18:44