
O circulo que nao fechava — flushSync e a View Transition do Portfolio
A transicao de tema do Portfolio sempre teve um problema sutil: quando o usuario clicava para trocar de dark para light, o circulo expansivo revelava… o tema dark ainda. Como se a animacao rodasse “sem o site no fundo”, um flash do tema antigo antes do novo aparecer.
O CSS estava certo, o clip-path estava certo, o ViewTransition estava certo. O problema era o React.
O snapshot que pegava o estado errado
O document.startViewTransition(callback) funciona assim: o browser tira um screenshot da pagina ANTES de executar o callback, e depois do callback, tira um screenshot do NOVO estado. A transicao anima entre esses dois frames.
O problema: no codigo original, o callback era:
const apply = () => {
setTheme(next);
};
O setTheme do React e assincrono. O React agenda o re-render, mas o browser ja capturou o snapshot “novo” antes do React ter commitado o estado. Resultado: o snapshot “novo” ainda mostrava o tema antigo.
A animacao rodava entre dois snapshots identicos — o circulo se abria e nada mudava. So depois, num frame separado, o React commitava o tema e o CSS aplicava, dando aquele flash estranho.
flushSync: o React commita na hora
A solucao veio do react-dom:
import { flushSync } from "react-dom";
const apply = () => {
flushSync(() => {
setTheme(next);
});
};
O flushSync força o React a commitar o estado sincronamente dentro do callback. Quando o browser tira o snapshot “novo”, o tema ja foi aplicado e o DOM ja reflete data-theme="light". O circulo revela o tema certo, sem flash.
Foi um commit de 14 linhas (58ba720) que resolveu um problema que existia ha meses. A linha chave:
// flushSync: o snapshot "novo" do ViewTransition so e capturado
// DEPOIS do React commitar o tema. Sem isso o circulo revela o
// tema antigo (animacao "sem o site no fundo").
flushSync(() => {
setTheme(next);
});
O resto da maratona de qualidade
A restauracao da View Transition circular veio acompanhada de um mutirao de qualidade no mesmo dia (91dd9c5):
Botoes icon-only na navbar. Samuel reportou que os botoes de tema, paleta e lingua tinham fundo glass desnecessario — ocupavam espaco visual sem agregar. Solucao: icon-only — sem fundo, sem padding extra, so o icone. A navbar respirou.
Teste de tema reescrito. O teste de toggle de tema era flaky — usava getAnimations() que as vezes resolvia antes da animacao terminar. O novo teste usa polling determinístico: espera o atributo data-theme mudar no <html> e o hook window.__pfThemeToggle ser chamado. 4 rodadas seguidas verdes, zero flaky.
Auditoria a11y de 24 cenarios. Um novo arquivo de teste (a11y-matrix.spec.ts) varre 3 rotas (home, sobre, projetos) em 2 idiomas (PT/EN), 2 temas (dark/light) e 2 viewports (desktop/mobile) — 24 combinacoes. Zero violacoes serias ou criticas em todas.
O que aprendi
-
startViewTransition+ React =flushSyncobrigatorio. O callback do VT e sincrono do ponto de vista do browser, mas o React nao commita estado na hora. SemflushSync, o snapshot “novo” captura o DOM antes do React aplicar a mudanca. -
Teste de animacao exige espera deterministica.
getAnimations()promete mais do que entrega. Olhar para o estado real do DOM (atributodata-theme, classe CSS) e mais confiavel que esperar promises de animacao. -
Qualidade veio em pacote. A correcao da VT veio junto com icon-only, a11y matrix e teste robusto. Uma correcao puxou a outra, e o resultado foi um commit de 284 linhas que melhorou o projeto em varias frentes.
O Portfolio agora tem uma transicao de tema fluida, que funciona no clique, no teclado e com prefers-reduced-motion. E o circulo finalmente fecha como deveria.
Proximo passo
A View Transition do Portfolio esta resolvida, mas a do LifeLog ainda usa uma implementacao diferente (inline script vs React). Talvez unificar as duas abordagens no futuro — mas por hoje, o circulo fechou.