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, CRA ou Vue CLI
- Next.js Pages Router e App Router — com os ajustes documentados neste guia
- Nuxt.js — com guard de SSR para evitar execução no servidor
- 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.
Antes de qualquer coisa, o widget verifica se window e document existem. Se não existirem (ambiente Node.js / SSR), o script inteiro é ignorado — zero execução no servidor. Além disso, a flag window._seebLoaded garante que, mesmo que o script seja injetado duas vezes (por exemplo, via hot reload ou StrictMode do React), o widget só inicializa uma vez.
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 App Router com carregamento tardio do script, o evento pode já ter disparado quando o script é finalmente executado.
#seeb-content-wrapper (com detecção de framework)
O widget chama wrapContent(), que primeiro verifica se existe um <div id="seeb-content-wrapper">. Se já existir, não faz nada. Se não existir, ele procura por raízes de framework conhecidas nesta ordem: #__next (Next.js), #__nuxt (Nuxt.js), #app (Vue / SPAs genéricos), [ng-version] ou getAllAngularRootElements()[0] (Angular). Se encontrar uma dessas raízes, cria o wrapper e move apenas esse elemento raiz para dentro dele — sem tocar em outros nós do body. Sem framework detectado, move todos os filhos do body como antes.
<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.
#__next), Nuxt.js (#__nuxt), Vue CLI e SPAs genéricos (#app) e Angular ([ng-version]) são todos detectados. O widget envolve apenas o elemento raiz do framework, sem mover os demais nós do body. A exceção é o Next.js App Router, cujo ciclo de vida SSR/hydration exige os ajustes adicionais documentados abaixo.
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 | ✓ Sim | O widget detecta #__next automaticamente e envolve apenas esse nó — sem ajustes manuais na maioria dos casos |
| Next.js App Router | ~ Com ajuste | Requer componente Client + wrapper no layout — veja o guia completo abaixo |
| Nuxt.js | ✓ Sim | O widget detecta #__nuxt automaticamente; em SSR, o guard interno bloqueia execução no servidor |
| Vue CLI | ✓ Sim | O widget detecta #app automaticamente |
| Angular | ~ Com ajuste | O widget detecta [ng-version] automaticamente; com Angular Universal (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 |
Leitor de Voz
Utiliza a API SpeechSynthesis do navegador para ler em voz alta o texto selecionado pelo usuário ou, se não houver seleção, todo o texto visível da página. Não requer nenhuma dependência externa ou backend.
| Recurso | Atalho | Descrição |
|---|---|---|
| Iniciar / Parar Leitura | Alt + V |
Lê o texto selecionado ou todo o conteúdo da página. Clique novamente para interromper. |
| Voz Feminina / Masculina | — | Filtra as vozes disponíveis no sistema por gênero. Fallback automático se não houver voz do gênero escolhido. |
| Velocidade de Leitura | — | Slider de 0.5x a 2.0x. Trocar velocidade enquanto lendo reinicia o áudio de forma fluida (sem fechar o painel). |
lang iniciando em pt e faz fallback para qualquer voz disponível. Em ambientes SSR, o leitor de voz nunca é executado — a API SpeechSynthesis não existe no Node.js.
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 |
| Ocultar Fundos | — | Remove todas as background-image CSS da página via background-image: none. Imagens <img> permanecem visíveis. Útil para leitura sem distrações visuais. |
| Parar Animações | Alt + P |
animation-play-state: paused e transition-duration: 0ms em todos os elementos |
| Realçar Foco | Alt + J |
Adiciona outline laranja visível (3px solid #FF6B00) em todos os elementos focados via teclado (:focus / :focus-visible). Essencial para navegação sem mouse. |
| Reduzir Transparência | — | Força opacity: 1 e remove backdrop-filter em todos os elementos, tornando sobreposições semi-transparentes completamente opacas. |
| Controle de Brilho | — | Slider de 50% a 150%. Aplica brightness() na cadeia de filtros CSS do wrapper de conteúdo. Composto com demais filtros ativos. |
| Controle de Saturação | — | Slider de 0% a 200%. Aplica saturate() na cadeia de filtros CSS. Útil para pessoas com sensibilidade a cores saturadas. |
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 + V | Leitor de Voz (iniciar/parar) |
Alt + J | Realçar Foco do Teclado |
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, hideBgImages: false, bigCursor: false, highlightLinks: false, pauseAnimations: false, highlightFocus: false, reduceTransparency: false, brightness: 100, saturation: 100, voiceRate: 1, voiceGender: 'female', 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 (Pages Router)
Com o Pages Router, o widget detecta automaticamente o elemento #__next e o envolve no wrapper — sem mover outros nós do body. Na maioria dos casos, adicionar o script no _document.tsx é suficiente.
import { Html, Head, Main, NextScript } from 'next/document' export default function Document() { return ( <Html lang="pt-BR"> <Head> {/* O widget detecta #__next automaticamente — sem ajuste extra */} <script src="https://seeb-widget.pages.dev/widget.js" defer /> </Head> <body> <Main /> <NextScript /> </body> </Html> ) }
#__next em wrapContent() e envolve apenas esse elemento — sem mover os scripts e outros nós do body. O React mantém todas as referências DOM intactas. Se o DOMContentLoaded já tiver disparado quando o script carregar, o widget ainda inicializa via o caminho de fallback imediato.
5.4 — 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) } })
No App Router, o Next.js não cria um elemento #__next — por isso a detecção automática de framework do widget não encontra a raiz. O widget cai no comportamento padrão: 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() { if (document.getElementById("seeb-content-wrapper")) return const wrapper = document.createElement("div") wrapper.id = "seeb-content-wrapper" // Detecta raiz de framework conhecida const frameworkRoot = document.getElementById("__next") // Next.js Pages Router || document.getElementById("__nuxt") // Nuxt.js || document.getElementById("app") // Vue / SPAs genéricos || document.querySelector("[ng-version]") // Angular if (frameworkRoot) { // Envolve apenas o elemento raiz — sem mover outros nós do body frameworkRoot.parentNode.insertBefore(wrapper, frameworkRoot) wrapper.appendChild(frameworkRoot) } else { // Fallback: move todos os filhos do body (App Router cai aqui) Array.from(document.body.childNodes).forEach(node => wrapper.appendChild(node) ) 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 |
App Router não gera #__next — widget cai no fallback e move todos os nós do body |
Pre-criar <div id="seeb-content-wrapper"> no layout (o widget encontra o div e não move nada) |
| Hydration mismatch de atributos | <Script> do Next.js gera data-nscript no servidor |
useEffect + createElement em vez do componente Script |
5.5 — Nuxt.js
O widget detecta automaticamente o elemento #__nuxt e o envolve no wrapper. Além disso, o guard interno de SSR impede qualquer execução no servidor Node.js — o script é completamente ignorado em contextos sem window ou document.
Em Nuxt 3 com configuração padrão, adicione o script via nuxt.config.ts:
export default defineNuxtConfig({ app: { head: { script: [ { src: 'https://seeb-widget.pages.dev/widget.js', defer: true, }, ], }, }, })
Ou via plugin client-only, se precisar de mais controle:
// O sufixo .client.ts garante que este plugin só executa no browser export default defineNuxtPlugin(() => { 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) })
<head> e o Nuxt renderize no servidor, o widget tem um guard inicial que verifica typeof window === 'undefined' e para imediatamente — zero execução no Node.js. No cliente, detecta #__nuxt e envolve apenas esse elemento.
5.6 — 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.7 — 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: O widget tem proteção interna via window._seebLoaded que impede inicializações duplicadas mesmo se o script for carregado duas vezes. Nos exemplos com useEffect, sempre verifique antes de criar: if (document.querySelector('seeb-widget')) return.
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. -
No Next.js App Router, pré-crie o
#seeb-content-wrapperno layout. O widget detecta automaticamente#__next(Pages Router),#__nuxt(Nuxt.js),#app(Vue / SPAs) e[ng-version](Angular) — esses frameworks não precisam do pré-create manual. Apenas o App Router, que não gera#__next, requer o wrapper no layout. -
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.