arrow_back Voltar para o SeeB

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

Cenários com risco real de conflito
  • 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.

html index.html
<!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>
Por que usar defer
O atributo defer 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.

0
Guard de SSR e proteção contra carregamento duplo

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.

1
Aguarda o 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.

2
Cria o wrapper #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.

3
Injeta o elemento <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.

Por que a maioria dos frameworks funciona sem ajuste manual
A detecção automática de raízes de framework no passo 2 resolve a maioria dos conflitos: Next.js Pages Router (#__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
NormalAlt + 1
ProtanopiaAlt + 2
DeuteranopiaAlt + 3
TritanopiaAlt + 4
MonocromáticoAlt + 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 contrasteAlt + 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).
Disponibilidade de vozes
A lista de vozes disponíveis depende do sistema operacional e do navegador. Nem todos os navegadores expõem vozes em português. O widget tenta primeiro vozes com 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.

Arrastar

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.

Presets (grade 3×3)

Oito posições predefinidas acessíveis pelo painel. O preset padrão é right-center.

Preset Posição
top-leftSuperior Esquerdo
top-centerSuperior Centro
top-rightSuperior Direito
left-centerEsquerdo Centro
right-centerDireito Centro padrão
bottom-leftInferior Esquerdo
bottom-centerInferior Centro
bottom-rightInferior 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 + AAbrir/Fechar painel
Alt + TAmpliar texto
Alt + EEspaçamento de texto
Alt + BNegrito
Alt + USublinhar links
Alt + VLeitor de Voz (iniciar/parar)
Alt + JRealçar Foco do Teclado
Alt + GGuia de leitura
Alt + FLinha de foco
Alt + CAlternar contraste alto/normal
Alt + IInverter cores
Alt + DModo escuro
Alt + HOcultar imagens
Alt + MCursor grande
Alt + KLinks destacados
Alt + PParar animações
Alt + RRedefinir tudo
Alt + 1Daltonismo: Normal
Alt + 2Daltonismo: Protanopia
Alt + 3Daltonismo: Deuteranopia
Alt + 4Daltonismo: Tritanopia
Alt + 5Daltonismo: Monocromático

Persistência

Todas as configurações são salvas em localStorage na chave seeb.settings. O objeto completo de estado persistido:

js localStorage — chave: seeb.settings
{
  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,
}
Redefinir Tudo
O atalho 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>.

css Variáveis internas do 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.

html index.html
<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á.

html index.html (raiz do projeto)
<!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>
Por que funciona sem problemas
Scripts com 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.

tsx pages/_document.tsx
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>
  )
}
Por que funciona sem ajuste
O widget detecta #__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.

1
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.

js widget.js (interno — não edite)
document.addEventListener("DOMContentLoaded", () => {
  if (!document.querySelector("seeb-widget")) {
    const widget = document.createElement("seeb-widget")
    document.body.appendChild(widget)
  }
})
2
Widget não detecta a raiz do App Router e move nós gerenciados pelo React

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:

js widget.js (interno — não edite)
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)
  }
}
3
Hydration mismatch de atributos

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:

Solução: dois ajustes pontuais que resolvem os três problemas acima.

1 Criar um Client Component com 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

tsx src/components/accessibility-widget.tsx
'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
}
Por que funciona
  • return null → servidor não renderiza nada, cliente também não → zero hydration mismatch
  • useEffect só roda no cliente, após a montagem → sem conflito com SSR
  • script.onload cria o widget manualmente, contornando o DOMContentLoaded que não disparou
  • Guard querySelector('seeb-widget') evita duplicação no React StrictMode
2 Pre-criar o 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

tsx src/app/layout.tsx
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>
  )
}
Por que funciona
  • O seeb-content-wrapper já 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:

ts 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:

ts plugins/seeb-widget.client.ts
// 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)
})
Dupla proteção SSR
Mesmo que o script seja incluído no <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.

html index.html
<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:

js src/main.js
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)
Cuidado com Teleport to="body"
Se sua aplicação usa <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.

html src/index.html
<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:

ts src/app/app.component.ts
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:

ts app.component.ts (com guard SSR)
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)
  }
}
Ressalvas específicas do Angular
  • Zone.js: scripts injetados via createElement fora 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 à diretiva script-src.
  • Angular Universal / SSR: sempre use o guard isPlatformBrowser antes de qualquer operação com document ou window. 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.

Widget não aparece na tela

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ó.

Hydration mismatch no Next.js

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.

Widget aparece duplicado

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.

Script bloqueado por CSP

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.

Widget não funciona em páginas específicas

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-wrapper no 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.

100% Gratuito, Para Sempre

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.

Zero Coleta de Dados

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.

Client-Side Only

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.

Open Source e Auditável

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.

Contribua com o projeto
O SeeB é desenvolvido por alunos da ETEC Parque Belém como projeto open source. Contribuições são bem-vindas: correções de bugs, novas funcionalidades, melhoria de compatibilidade com frameworks ou aprimoramentos desta documentação. Acesse o repositório em github.com/devrafcks/seeb-widget.