Documentação Oficial do Widget
Guia completo de integração do widget de acessibilidade SeeB — da instalação básica em HTML estático até frameworks modernos com SSR. Inclui causas, soluções e exemplos de código prontos para produção.
Introdução
O que é o SeeB Widget
O SeeB Widget é um painel de acessibilidade client-side que você adiciona a qualquer site com uma única tag <script>. Ele injeta um Custom Element (<seeb-widget>) no DOM, expondo controles de acessibilidade aos usuários finais: ajuste de tamanho de fonte, contraste, filtros de daltonismo, modo de foco e espaçamento de texto.
Tudo acontece no navegador. O widget não faz chamadas externas durante o uso, não coleta dados e não depende de nenhum backend. As preferências do usuário são salvas em localStorage.
Quando usar
O widget é adequado para qualquer projeto que renderize HTML no browser sem restrições severas sobre manipulação do DOM global. Funciona bem em:
- Sites estáticos, blogs e landing pages
- SPAs com React, Vue ou Svelte via Vite ou CRA
- Next.js App Router — com os dois ajustes documentados neste guia
- Aplicações Angular — com as ressalvas desta documentação
-
Qualquer projeto onde você controla o
index.html
Quando não usar
- Projetos que manipulam diretamente os filhos imediatos do
<body>por fora do framework — o widget cria um wrapper em torno deles e pode desorganizar referências DOM - Ambientes com CSP (Content Security Policy) restritivo que bloqueia scripts de domínios externos sem allowlist explícita
- Microfrontends com múltiplos runtimes independentes gerenciando o mesmo
<body> - Projetos que já possuem outro widget de acessibilidade que também manipula o DOM global do body
Instalação Básica
Para a maioria dos projetos, adicionar o script é suficiente. O widget se auto-inicializa quando o DOM estiver pronto.
<!DOCTYPE html> <html lang="pt-BR"> <head> <meta charset="UTF-8"> <title>Meu Site</title> <!-- Funciona no <head> com defer ou antes de </body> --> <script src="https://seeb-widget.pages.dev/widget.js" defer ></script> </head> <body> <!-- Seu conteúdo aqui --> </body> </html>
deferdefer faz o browser baixar o script em paralelo com o HTML, mas executa apenas após o parsing completo do documento. Isso evita bloqueio de render e garante que o DOM está disponível quando o widget inicializa. Colocar o script antes de </body> sem defer tem efeito equivalente em HTML estático — mas defer no <head> é preferível porque permite que o download comece mais cedo.
Como funciona internamente
Entender o que o widget faz ao inicializar é essencial para prever e evitar conflitos em ambientes mais complexos.
DOMContentLoaded
O widget registra um listener para o evento DOMContentLoaded. Quando dispara, ele verifica se um <seeb-widget> já existe no DOM. Se não existir, cria e injeta no <body>. Em HTML estático e SPAs com Vite, esse evento sempre dispara antes da execução do widget — não há problema. Em Next.js com SSR e carregamento tardio do script, o evento pode já ter disparado quando o script é finalmente executado.
#seeb-content-wrapper
O widget chama wrapContent(), que verifica se existe um <div id="seeb-content-wrapper"> no body. Se não existir, ele cria esse div e move todos os filhos atuais do <body> para dentro dele. Isso isola o conteúdo da página para que estilos de acessibilidade possam ser aplicados de forma controlada. O problema surge quando os nós movidos são gerenciados por um framework — o React ou Angular ainda referencia as posições originais e falha ao tentar operá-los depois.
<seeb-widget>
O Custom Element é adicionado diretamente ao <body> — fora do wrapper. Ele contém o painel de controles via Shadow DOM e toda a lógica de persistência de preferências em localStorage. Opera como overlay independente e não interfere com o conteúdo da página.
<div id="seeb-content-wrapper"> antes que o widget inicialize. Com o div já existente, o widget encontra a condição if (!wrapper) como false e não move nenhum nó do DOM.
Compatibilidade por ambiente
| Ambiente | Funciona? | Observação |
|---|---|---|
| HTML Estático | ✓ Sim | Adicione o <script defer> no <head> |
| React + Vite / CRA | ✓ Sim | Adicione no index.html da raiz |
| Vue + Vite | ✓ Sim | Adicione no index.html; atenção se usar Teleport to="body" |
| Astro | ✓ Sim | Adicione no layout base com is:inline |
| Next.js Pages Router | ~ Com ajuste | Pre-criar o #seeb-content-wrapper no _document.tsx |
| Next.js App Router | ~ Com ajuste | Requer componente Client + wrapper no layout — veja o guia completo abaixo |
| Angular | ~ Com ajuste | Funciona na maioria dos casos; com SSR requer guard de plataforma |
| SvelteKit (SSR) | ~ Com ajuste | Use browser guard do SvelteKit antes de injetar o script |
Funcionalidades
O widget expõe um painel completo de acessibilidade com controles agrupados em cinco categorias. Todas as preferências são salvas automaticamente em localStorage e restauradas na próxima visita. Cada funcionalidade tem um atalho de teclado próprio com o modificador Alt +.
Daltonismo
Aplica filtros SVG feColorMatrix combinados com outros filtros ativos via inline style. Os filtros de daltonismo, contraste, inverter e modo escuro são sempre compostos numa única propriedade filter, eliminando conflitos de especificidade CSS.
| Opção | Atalho |
|---|---|
| Normal | Alt + 1 |
| Protanopia | Alt + 2 |
| Deuteranopia | Alt + 3 |
| Tritanopia | Alt + 4 |
| Monocromático | Alt + 5 |
Contraste
Ativa alto contraste via contrast(1.5) saturate(1.5). Mutuamente combinável com os demais filtros na mesma propriedade filter.
| Opção | Atalho |
|---|---|
| Normal | — |
| Alto contraste | Alt + C |
Ampliação e Leitura
Conjunto de controles que melhoram a legibilidade do conteúdo sem alterar o layout da página.
| Funcionalidade | Atalho | Descrição |
|---|---|---|
| Ampliar Texto | Alt + T |
Alterna font-size entre 100% e 125% no <html> |
| Espaçamento | Alt + E |
word-spacing: 2px, letter-spacing: 0.12em, line-height: 1.8 |
| Negrito | Alt + B |
font-weight: 700 em todos os elementos do conteúdo |
| Sublinhar Links | Alt + U |
text-decoration: underline com thickness: 2px em todos os <a> |
| Guia de Leitura | Alt + G |
Régua horizontal que segue o mouse, com throttle via requestAnimationFrame. Altura configurável via slider (10px a 200px) |
| Linha de Foco | Alt + F |
Destaca o elemento sob o cursor com cor de fundo configurável via color picker |
Visibilidade
Controles que alteram a aparência visual da página inteira. Imagens e vídeos são re-invertidos automaticamente nos modos que aplicam inversão, para manter sua aparência natural.
| Funcionalidade | Atalho | Descrição |
|---|---|---|
| Inverter Cores | Alt + I |
filter: invert(1) no wrapper de conteúdo. Imagens e vídeos são re-invertidos para manter aparência natural |
| Modo Escuro | Alt + D |
filter: invert(0.9) hue-rotate(180deg). Imagens e vídeos re-invertidos. Mutuamente exclusivo com Inverter Cores |
| Ocultar Imagens | Alt + H |
visibility: hidden em img, video e picture |
| Parar Animações | Alt + P |
animation-play-state: paused e transition-duration: 0ms em todos os elementos |
Interação
Recursos que modificam como o usuário interage com a página, facilitando a navegação por teclado e mouse.
| Funcionalidade | Atalho | Descrição |
|---|---|---|
| Cursor Grande | Alt + M |
Substitui o cursor por um SVG de 32×32px via CSS custom cursor |
| Links Destacados | Alt + K |
Adiciona outline, background e underline em todos os <a> |
Configuração
Aparência
As cores do widget são configuráveis diretamente no painel pelo usuário final e salvas no localStorage. As variáveis CSS internas do widget também podem ser sobrescritas via tema.
| Campo | CSS Variable | Padrão |
|---|---|---|
| Cor do Ícone e Cabeçalho | --cor-principal, --cor-fundo-icone |
#003667 |
| Cor do Fundo do Painel | --cor-fundo-panel |
#E6EDF3 |
Posição do Widget
O ícone flutuante pode ser reposicionado de duas formas: arrastando livremente ou escolhendo um preset na grade 3×3 dentro do painel.
Clique e arraste o ícone para qualquer lugar da tela. O drag usa throttle com requestAnimationFrame para performance. Se o usuário soltar sem mover mais de 5px, o painel abre normalmente — o clique e o drag coexistem sem conflito.
Oito posições predefinidas acessíveis pelo painel. O preset padrão é right-center.
| Preset | Posição |
|---|---|
top-left | Superior Esquerdo |
top-center | Superior Centro |
top-right | Superior Direito |
left-center | Esquerdo Centro |
right-center | Direito Centro padrão |
bottom-left | Inferior Esquerdo |
bottom-center | Inferior Centro |
bottom-right | Inferior Direito |
Atalhos de Teclado
Todos os controles do widget têm atalho de teclado usando Alt + como modificador. Os atalhos funcionam globalmente na página, independentemente do foco atual.
| Atalho | Ação |
|---|---|
Alt + A | Abrir/Fechar painel |
Alt + T | Ampliar texto |
Alt + E | Espaçamento de texto |
Alt + B | Negrito |
Alt + U | Sublinhar links |
Alt + G | Guia de leitura |
Alt + F | Linha de foco |
Alt + C | Alternar contraste alto/normal |
Alt + I | Inverter cores |
Alt + D | Modo escuro |
Alt + H | Ocultar imagens |
Alt + M | Cursor grande |
Alt + K | Links destacados |
Alt + P | Parar animações |
Alt + R | Redefinir tudo |
Alt + 1 | Daltonismo: Normal |
Alt + 2 | Daltonismo: Protanopia |
Alt + 3 | Daltonismo: Deuteranopia |
Alt + 4 | Daltonismo: Tritanopia |
Alt + 5 | Daltonismo: Monocromático |
Persistência
Todas as configurações são salvas em localStorage na chave seeb.settings. O objeto completo de estado persistido:
{ fontSize: 100, fontSizeIncreased: false, textSpacing: false, boldText: false, underlineLinks: false, readingGuide: false, rulerHeight: 50, focusLine: false, focusLineColor: '#AED5F2', colorBlindness: 'normal', contrast: 'normal', invertColors: false, darkMode: false, hideImages: false, bigCursor: false, highlightLinks: false, pauseAnimations: false, widgetIconColor: '#003667', widgetPanelColor: '#E6EDF3', widgetPositionPreset: 'right-center', widgetPositionX: null, widgetPositionY: null, }
Alt + R remove a chave seeb.settings do localStorage e restaura todos os valores para o estado padrão listado acima. Nenhum outro dado do localStorage da aplicação é afetado.
CSS Variables
As variáveis CSS internas do widget operam dentro do Shadow DOM. Elas controlam a aparência do painel e do ícone flutuante e podem ser alteradas programaticamente via JavaScript após a criação do elemento <seeb-widget>.
:root { --cor-principal: #003667; --cor-destaque: #014C83; --cor-neutra-clara: #E6EDF3; --cor-neutra-escura: #1A4975; --cor-fundo-panel: var(--cor-neutra-clara); --cor-fundo-icone: var(--cor-principal); --cor-texto-claro: #FFFFFF; --cor-borda-padrao: #C3D2E0; --cor-borda-foco: #315A81; --cor-fundo-hover: #D6E6F2; --fonte-principal: 'Atkinson Hyperlegible', sans-serif; }
Guia por Framework
5.1 — HTML Puro
O caso mais simples. Adicione a tag <script defer> no <head>. Sem passos adicionais — o widget inicializa automaticamente quando o DOM estiver pronto e aparece no canto inferior da tela.
<head> <!-- Demais tags meta, CSS, etc. --> <script src="https://seeb-widget.pages.dev/widget.js" defer ></script> </head>
5.2 — Vite (React, Vue, Svelte)
Projetos Vite têm um index.html na raiz que serve como ponto de entrada. Adicione o script diretamente nele — o Vite não processa essa tag, ela vai ao browser exatamente como está.
<!DOCTYPE html> <html lang="pt-BR"> <head> <meta charset="UTF-8"> <title>Meu App</title> <script src="https://seeb-widget.pages.dev/widget.js" defer></script> </head> <body> <div id="root"></div> <script type="module" src="/src/main.tsx"></script> </body> </html>
defer e scripts type="module" são diferidos pelo browser. O widget inicializa no DOMContentLoaded — que em projetos Vite dispara antes do bundle do framework executar. Na prática, o wrapper é criado antes que o React ou Vue montem, então não há nós gerenciados pelo framework para o widget mover.
5.3 — Next.js (App Router)
O App Router do Next.js tem um ciclo de vida que entra em conflito com a forma como o widget inicializa. São três problemas independentes — cada um com uma causa clara e uma solução pontual.
DOMContentLoaded não dispara para o widget
Com strategy="afterInteractive" do componente <Script> do Next.js, o script só carrega depois da hydration do React. O evento DOMContentLoaded já disparou quando o script chega — o listener do widget nunca executa e o <seeb-widget> nunca é criado.
document.addEventListener("DOMContentLoaded", () => { if (!document.querySelector("seeb-widget")) { const widget = document.createElement("seeb-widget") document.body.appendChild(widget) } })
Quando o widget chama wrapContent(), ele move todos os filhos do <body> para dentro de um novo <div id="seeb-content-wrapper">. Esses nós incluem o que o React criou e ainda referencia internamente. Quando o React tenta operar nesses nós (por exemplo, ao navegar entre rotas), não os encontra mais onde esperava:
wrapContent() { let wrapper = document.getElementById("seeb-content-wrapper") if (!wrapper) { wrapper = document.createElement("div") wrapper.id = "seeb-content-wrapper" Array.from(document.body.childNodes).forEach(node => { wrapper.appendChild(node) // move nós gerenciados pelo React }) document.body.appendChild(wrapper) } }
Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.
O componente <Script> do Next.js injeta atributos como data-nscript no HTML do servidor. Com a prop onLoad (executada apenas no cliente), esses atributos divergem entre server e client durante a hydration:
A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.
Solução: dois ajustes pontuais que resolvem os três problemas acima.
useEffect
Em vez do componente <Script> do Next.js, crie um Client Component que retorna null no servidor (zero HTML gerado) e injeta o script via useEffect no cliente. O callback onload do script cria o <seeb-widget> manualmente — contornando o DOMContentLoaded que não disparou.
Resolve os problemas: 1 e 3
'use client' import { useEffect } from 'react' export function AccessibilityWidget() { useEffect(() => { if (document.querySelector('seeb-widget')) return const script = document.createElement('script') script.src = 'https://seeb-widget.pages.dev/widget.js' script.defer = true script.onload = () => { if (!document.querySelector('seeb-widget')) { const widget = document.createElement('seeb-widget') document.body.appendChild(widget) } } document.body.appendChild(script) }, []) return null }
return null→ servidor não renderiza nada, cliente também não → zero hydration mismatchuseEffectsó roda no cliente, após a montagem → sem conflito com SSRscript.onloadcria o widget manualmente, contornando oDOMContentLoadedque não disparou- Guard
querySelector('seeb-widget')evita duplicação no React StrictMode
seeb-content-wrapper no layout
Adicione o <div id="seeb-content-wrapper"> diretamente no layout do servidor. Quando o widget inicializar no cliente e chamar wrapContent(), o div já existe — a condição if (!wrapper) é false e nenhum nó é movido.
Resolve o problema: 2
import type { Metadata } from 'next' import { AccessibilityWidget } from '@/components/accessibility-widget' export const metadata: Metadata = { title: 'Minha Aplicação', } export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( <html lang="pt-BR"> <body> {/* O wrapper deve existir ANTES do widget inicializar */} <div id="seeb-content-wrapper"> {children} </div> <AccessibilityWidget /> </body> </html> ) }
- O
seeb-content-wrapperjá existe no HTML quando o widget inicializa no cliente - O widget encontra o div, entra na condição
if (!wrapper)como false e não move nenhum nó - O React mantém todas as referências DOM intactas durante a navegação
| Problema | Causa | Solução |
|---|---|---|
| Widget não aparece | DOMContentLoaded já disparou quando o script carrega |
Criar <seeb-widget> manualmente no onload |
removeChild — node is not a child |
Widget move nós gerenciados pelo React para dentro do wrapper | Pre-criar <div id="seeb-content-wrapper"> no layout |
| Hydration mismatch de atributos | <Script> do Next.js gera data-nscript no servidor |
useEffect + createElement em vez do componente Script |
5.4 — Vue
A maioria dos projetos Vue usa Vite. Nesses casos, a abordagem mais simples e confiável é adicionar o script diretamente no index.html da raiz.
<head> <script src="https://seeb-widget.pages.dev/widget.js" defer></script> </head> <body> <div id="app"></div> <script type="module" src="/src/main.js"></script> </body>
Se preferir injetar programaticamente após a montagem do app — para garantir que o Vue já assumiu o DOM antes do widget inicializar:
import { createApp } from 'vue' import App from './App.vue' const app = createApp(App) app.mount('#app') // Injetar após mount — Vue já controla o DOM nesse ponto const script = document.createElement('script') script.src = 'https://seeb-widget.pages.dev/widget.js' script.defer = true document.body.appendChild(script)
Teleport to="body"<Teleport to="body"> para modais ou overlays, esses elementos são filhos imediatos do <body> — e o widget pode movê-los para dentro do wrapper, causando conflitos. A solução é pré-criar o <div id="seeb-content-wrapper"> no index.html e usar <Teleport to="#seeb-content-wrapper"> como destino em vez do body diretamente.
5.5 — Angular
A abordagem com menor risco é adicionar o script diretamente no src/index.html do projeto Angular. O widget inicializa antes do bootstrap do framework, o que evita conflitos com o ciclo de vida do Angular.
<head> <script src="https://seeb-widget.pages.dev/widget.js" defer ></script> </head> <body> <app-root></app-root> </body>
Para controlar o carregamento via TypeScript — por exemplo, para evitar injeção em ambientes de teste ou aplicar condições específicas:
import { Component, OnInit } from '@angular/core' @Component({ selector: 'app-root', templateUrl: './app.component.html', }) export class AppComponent implements OnInit { ngOnInit(): void { if (document.querySelector('seeb-widget')) return const script = document.createElement('script') script.src = 'https://seeb-widget.pages.dev/widget.js' script.defer = true document.body.appendChild(script) } }
Se o projeto usa Angular Universal (SSR), adicione um guard de plataforma antes de qualquer operação de DOM:
import { Component, OnInit, Inject, PLATFORM_ID } from '@angular/core' import { isPlatformBrowser } from '@angular/common' @Component({ selector: 'app-root', templateUrl: './app.component.html' }) export class AppComponent implements OnInit { constructor(@Inject(PLATFORM_ID) private platformId: object) {} ngOnInit(): void { // Nunca executar operações de DOM no servidor (Node.js) if (!isPlatformBrowser(this.platformId)) return if (document.querySelector('seeb-widget')) return const script = document.createElement('script') script.src = 'https://seeb-widget.pages.dev/widget.js' script.defer = true document.body.appendChild(script) } }
- Zone.js: scripts injetados via
createElementfora do contexto do Angular não são interceptados pelo zone.js. O widget não dispara detecção de mudanças do Angular — isso é esperado e inofensivo, pois o widget opera de forma completamente independente. - CSP restritivo: se o projeto Angular usa Content Security Policy, adicione
https://seeb-widget.pages.devà diretivascript-src. - Angular Universal / SSR: sempre use o guard
isPlatformBrowserantes de qualquer operação comdocumentouwindow. O código do widget não deve executar no contexto Node.js.
Problemas Conhecidos
Referência rápida para os erros mais frequentes durante a integração.
Causa: O evento DOMContentLoaded já disparou quando o script foi carregado. Comum em Next.js App Router com strategy="afterInteractive" e em situações de lazy loading tardio.
Solução: Crie o elemento <seeb-widget> manualmente no callback onload do script, como demonstrado no guia do Next.js. O padrão de criação manual funciona em qualquer framework.
removeChild — The node is not a child of this node
Causa: O widget chamou wrapContent() e moveu nós DOM que o framework (React, Angular) ainda referencia internamente. Quando o framework tentou operar nesses nós, não os encontrou mais no <body>.
Solução: Pré-crie o <div id="seeb-content-wrapper"> antes que o widget inicialize. Com o div existente, o widget não move nenhum nó.
Causa: O componente <Script> do Next.js com onLoad gera atributos distintos no HTML do servidor e no cliente, causando divergência na hydration do React.
Solução: Use o padrão useEffect + createElement. O componente retorna null no servidor, eliminando qualquer possibilidade de mismatch de atributos.
Causa: O script foi injetado ou o widget foi criado mais de uma vez. Acontece quando hooks de ciclo de vida executam múltiplas vezes (React StrictMode executa os efeitos duas vezes em desenvolvimento), ou quando o componente que inicia o widget é montado e desmontado durante a navegação.
Solução: Sempre verifique antes de criar: if (document.querySelector('seeb-widget')) return. Todos os exemplos nesta documentação já incluem esse guard.
Causa: O projeto tem uma Content Security Policy que não permite carregar scripts de domínios externos não listados.
Solução: Adicione https://seeb-widget.pages.dev à diretiva script-src do seu CSP. Como alternativa, faça o self-hosting do widget.js a partir do repositório público.
Causa: Algumas páginas usam tecnologias proprietárias ou estruturas de DOM não convencionais que impedem o widget de aplicar seus estilos corretamente. É um comportamento esperado para casos extremos — não um bug.
Solução: Reporte a página no repositório via issue com o domínio e uma descrição do problema. A equipe avalia caso a caso.
Boas Práticas
-
Sempre use
defer. Evita bloqueio de render e garante que o DOM está disponível quando o widget executa. -
Em projetos com SSR, pré-crie o
#seeb-content-wrapper. Isso previne a movimentação de nós DOM gerenciados pelo framework — o ajuste mais importante para Next.js, SvelteKit e Angular Universal. -
Aplique sempre o guard de verificação antes de criar:
if (document.querySelector('seeb-widget')) return. Previne duplicações em hot reload, StrictMode e navegações em SPA. - Carregue o widget uma única vez no ponto de entrada. Em SPAs, não coloque a lógica de injeção em componentes que montam e desmontam durante a navegação. Use o layout raiz ou o arquivo de bootstrap.
-
Não manipule os filhos imediatos do
<body>manualmente ao mesmo tempo que o widget está ativo. Se o código da aplicação move, clona ou remove nós do body por fora do framework, pode conflitar com o wrapper do widget. - Valide em produção. O comportamento com SSR, hydration e carregamento assíncrono pode diferir entre desenvolvimento e produção. Teste que o widget aparece corretamente após o build, incluindo em rotas com SSR.
- Em projetos com CSP, configure o allowlist antes de implantar em produção. Um CSP que bloqueia scripts externos silencia o widget sem gerar erros visíveis ao usuário.
Filosofia do Projeto
O SeeB Widget existe porque acreditamos que acessibilidade não deveria ser um projeto paralelo — ela já deveria estar lá, embutida na experiência padrão da web.
Não existe plano pago, tier premium, limite de uso ou intenção de monetização. O widget é gratuito para qualquer projeto — pessoal, comercial ou educacional. Não há roadmap de cobrança.
O widget não faz nenhuma chamada externa durante o uso do usuário. Sem analytics, telemetria, rastreamento ou envio de informações a servidores. As preferências de acessibilidade são salvas apenas em localStorage — no dispositivo do próprio usuário. Nenhum dado sai do navegador.
Todo o processamento acontece no navegador. Não há backend, não há dependência de infraestrutura externa além do carregamento inicial do widget.js. Uma vez carregado, funciona offline. Falhas de rede após o carregamento inicial não afetam o funcionamento.
O código-fonte está disponível publicamente. Qualquer desenvolvedor pode auditar o que o widget faz, propor melhorias ou reportar problemas. Não há código ofuscado ou comportamento escondido. O que está documentado aqui reflete exatamente o que o código faz.