Duas fileiras de fotos paisagem grandes atravessando a tela em sentidos opostos, uma acima e outra abaixo de uma frase centrada. Não são logos nem miniaturas: são imagens de 432px de largura em 16:10, com o lugar de cada uma escrito numa pílula dentro da própria foto. As duas andam sozinhas, devagar, e reagem à rolagem — quem passa rápido pela dobra vê as fileiras acelerarem até quase sete vezes o ritmo de cruzeiro e as fotos inclinarem uns graus; quem para vê tudo voltar ao passo normal em cerca de um quarto de segundo. O olho não escolhe uma foto: ele atravessa a dobra junto com elas, e o que fica é a impressão de quantidade e de lugar.
Resolve a dobra de "onde estamos" quando o material é foto e não mapa. Uma grade de seis imagens conta o mesmo, mas conta como catálogo: cada foto vira um item a examinar, e a pessoa para em uma delas. A esteira troca o inventário pela presença — mostra dez lugares sem pedir que ninguém olhe dez lugares, e o texto no meio fica sendo a única coisa que se lê. A reação à rolagem é o que impede a dobra de ser só decoração: ela responde ao gesto de quem está passando, e por isso lê como parte da página e não como um GIF colado nela.
Quando usar
- Dobra de presença, território, cobertura, bastidor: "onde a gente está", "como a gente trabalha", "por onde a gente anda".
- Quando há dez fotos boas de verdade, todas do mesmo ensaio de cor, e nenhuma delas precisa ser clicada.
- Quando o argumento da dobra cabe em três linhas centradas e as imagens são atmosfera, não informação.
- Em página longa: a dobra tem 1 122px de altura em desktop, e é uma respiração entre duas seções de texto.
Quando não usar
- Fotos que a pessoa precisa abrir, comparar ou escolher. Nada aqui é clicável, e nada deveria ser: alvo que anda é alvo que se erra. Use
midia/galeria-scroll-horizontal, onde o card é um<a>de verdade e o encaixe é nativo, ouoverlay/card-expansivel-fullscreense o clique tem de abrir em tela cheia. - Logos de instituição. É outro componente e outra proporção:
prova/marquee-logos. - Pessoas com nome, cargo e bio.
midia/carrossel-perfis— lá o trilho para sob o ponteiro justamente para a bio ser lida; aqui nada para, porque não há o que ler. - Depoimento ou texto que atravessa.
prova/carrossel-depoimentosfaz isso em CSS puro, sem script nenhum. - Uma foto só, grande.
hero/full-bleed-media. - Menos de cinco fotos por esteira. O laço fica curto e a repetição vira o assunto da dobra. Com quatro, o segundo conjunto já entra em cena antes de o primeiro sair.
Padrão editorial
| Slot | Regra |
|---|---|
| Olho | Uma palavra, no máximo duas. É rótulo de dobra, não frase. |
| Título | Uma linha que quebra em duas, 30 a 42 caracteres. Uma palavra em <em> — o realce em itálico na cor de marca. |
| Apoio | Uma frase, 120 a 180 caracteres. Ela é o único texto que alguém vai ler de verdade nesta dobra: tem de dizer o argumento inteiro sozinha. |
| Fotos por esteira | Exatamente 5, e as duas esteiras com fotos diferentes. Menos de 5 encurta o laço; mais de 5 só adia o problema de arrumar boas fotos. |
| Legenda | Até 24 caracteres. Lugar e recorte, separados por travessão: "Vinhedo — Serra Gaúcha". Sem verbo, sem frase, sem ponto final. |
| Ordem | As cinco primeiras de cada esteira são as melhores. Com "reduzir movimento" ligado, as esteiras congelam no começo da fila, e são essas as únicas que alguém vê. |
O que mantém a dobra coesa é o paralelismo das legendas: se uma nomeia cidade, todas nomeiam; se uma nomeia atividade, todas nomeiam. Legenda de tipo misturado ("Faria Lima — SP" ao lado de "Onde tudo começou") faz a fileira parecer duas coisas diferentes passando juntas. E as duas esteiras têm de contar o mesmo assunto em ordens diferentes — não uma o Brasil e outra o exterior, porque em movimento ninguém percebe a divisão.
Especificação dos assets
- Formato e dimensão mínima: paisagem 1600×900 ou maior, WebP. O quadro exibido é 16:10 e o
object-fit:covercorta em cima e embaixo — o assunto tem de estar na faixa central. - Quantidade: 10 fotos, 5 por esteira. No preview as cinco paisagens de demonstração da biblioteca aparecem nas duas, em ordens diferentes, porque só existem cinco.
- Peso alvo: até 180 KB por foto. A dobra carrega todas de imediato (ver o gotcha do
loading="lazy"), então 10 fotos são ~1,8 MB adiantados. - Composição / enquadramento: plano aberto, um assunto só, horizonte no terço. Foto que precisa ser examinada não funciona aqui — ela passa em pouco mais de dez segundos.
- Cor: todas do mesmo tratamento. Uma foto mais saturada que as outras vira a única coisa que se vê na fileira inteira.
- O que não pode ter: texto dentro da imagem (a legenda já mora ali e as duas brigam), rosto reconhecível em primeiro plano sem autorização, logo de instituição real, e nada com linha de horizonte muito marcada — é o enquadramento que mais denuncia a inclinação.
Como usar
<link rel="stylesheet" href="componentes/midia/esteira-dupla-imagens/estilo.css">
<script src="componentes/midia/esteira-dupla-imagens/script.js"></script>
O <script> é opcional: sem ele as duas esteiras andam por animação de CSS. O que se perde é a velocidade em px/s e a reação à rolagem.
Ganchos
| Gancho | Papel |
|---|---|
[data-flux] |
Raiz. O script parte daqui, nunca do documento, e é aqui que ele escreve --flux-skew. |
[data-flux-rail="-1"] / ="1" |
O recorte de cada esteira e o sentido dela. Uma fonte só: o CSS e o script leem o mesmo atributo. |
[data-flux-track] |
O que anda. Tem de conter exatamente dois .flux-set idênticos, o segundo com aria-hidden. |
--flux-card / --flux-gap |
Largura da foto e vão até a próxima. clamp(240px,30vw,440px) e clamp(14px,1.6vw,24px). |
--flux-space |
Respiro entre esteira e texto. |
--flux-veil |
Altura do véu, em fração da foto. 38%. |
--flux-dur |
Volta completa da animação de CSS — o ritmo de quem não tem JavaScript. 56s. |
--flux-speed |
Velocidade de cruzeiro do script, em px/s. 40. 0 deixa tudo parado. |
--flux-react |
Fração da velocidade da página que vira impulso. .35 no desktop, .2 no toque. |
--flux-boost |
Teto do impulso, em múltiplos de --flux-speed. 6. |
--flux-skew-max |
Inclinação máxima da foto, no teto do impulso. 6deg no desktop, 4deg no toque. |
--flux-skew |
Escrita pelo script na raiz. 0deg sem script. |
--flux-dir / --flux-dir-back |
Sentido da esteira e o inverso: as fotos inclinam por um, a legenda endireita pelo outro. |
.is-driven |
Na raiz: o script assumiu e a animação de CSS está desligada. |
Variações
- esteiras paradas —
--flux-speed:0. Nada anda e nada reage: o teto do impulso é múltiplo da base. - sem reação à rolagem —
--flux-react:0. Fica o ganho de andar em px/s. - só acelera, não inclina —
--flux-skew-max:0deg. Para tema de canto arredondado e para foto com horizonte forte. - uma esteira só — apague um
.flux-rail. O script trata cada uma em separado. - em seção clara —
.sec-paperno lugar de.sec-dark. O que está sobre a foto não muda: é ilha escura. - direção segue o scroll — uma linha no script (ver
componente.json). Rolar para cima inverte as fileiras.
Gotchas
-
O script lê a posição da animação de CSS antes de desligá-la, e é isso que faz a troca de mãos ser invisível.
getComputedStyle(el).transformdevolve a matriz animada do quadro corrente, não a declarada — dá para converter em deslocamento e continuar dali. Medido: depois de 3s de animação de CSS o trilho estava em −117,97px e o script assumiu em −118,48px no quadro seguinte, ou seja 0,51px, que é exatamente o que ele deveria ter andado nesse intervalo. Sem a leitura, o salto seria os 118px inteiros — e no meio da fileira, onde a máscara das pontas não cobre nada. A leitura, otransforminline e o.is-drivensão escritos na mesma tarefa, de propósito: separados, sobra um quadro com a animação desligada e nenhum transform, que é o mesmo pulo a zero por outro caminho. -
--flux-dure--flux-speedsão o mesmo parafuso em unidades diferentes, e nunca casam em toda largura. A conversão édur = largura do conjunto ÷ speed. Medido a 1440px: o conjunto tem 2 275,16px, logo 2275/40 = 56,7s, e daí o padrão de56s. Mas duração fixa quer dizer velocidade variável: a 390px o conjunto cai para 1 308px e a mesma animação de 56s anda a 23,4px/s, 58% do ritmo do desktop. O script, contando em px/s, mede 40,0px/s em 1440 e 40,0px/s em 390. Essa é a razão principal de ele assumir — a reação à rolagem é a segunda. -
A velocidade é por tempo, nunca por quadro — e o decaimento do impulso também. O laço multiplica por
dt, com teto de 50ms para a volta de uma aba em segundo plano não saltar meia volta. O decaimento éMath.pow(.92, dt*60), e nãoboost *= .92: escrito cru, uma tela de 120Hz devolveria o ritmo no dobro da velocidade. Meia-vida medida: ~0,14s. A prova de que funciona é o ambiente do medidor, que roda a 4,26ms por quadro (≈235Hz) e ainda assim entrega os 40,0px/s pedidos. -
O vão entre fotos não muda quando elas inclinam, e a caixa envolvente mente sobre isso.
skewXdesloca cada ponto pory·tanθ; como toda a fileira tem a mesma altura e o mesmotransform-origin, duas fotos vizinhas se deslocam pelo mesmo tanto em cada altura. Medido nas arestas reais: 23px no topo, no meio e na base, a 0°, 3° e 6°. Já ogetBoundingClientRectde cada foto engorda 14,19px de cada lado (é a caixa que envolve o paralelogramo) e passa a acusar uma sobreposição de −5,35px a 6° — sobreposição que não existe. Quem for medir isto de novo: meça as arestas, não as caixas. -
A inclinação é do quadro inteiro, e a legenda tem de ser des-inclinada de volta. Inclinar só a
<img>a descolaria das bordas do quadro e abriria fresta do fundo nos dois cantos. Inclinando o quadro, o rótulo tomba junto — e rótulo tombado lê como página quebrada, não como velocidade. Daí--flux-dir-back: a pílula recebe oskewXoposto, comtransform-origin:0 100%, que é o mesmo canto em que ela está ancorada, então ela endireita sem sair do lugar. -
skewXsobreoverflow:hidden+border-radiusfoi investigado e o serrilhado não apareceu. Com--radius-media:16pxe 6° de inclinação, inspecionado a 3× de zoom, o Chrome antisserrilha o recorte arredondado limpo. A medição é headless, DPR 1: o risco continua real em princípio (o recorte é rasterizado e depois transformado pelo compositor), e a saída, se um dia aparecer numa tela de verdade, é--flux-skew-max:0deg— a aceleração continua, só o tombo some. -
As fotos não levam
loading="lazy", e não é esquecimento. O critério do lazy é a posição de layout da imagem, e aqui as fotos entram em cena portransform, sem mudar de posição no layout: uma foto adiada pode nunca disparar o carregamento e atravessar a esteira como retângulo vazio. O que torna isso barato é o segundo conjunto repetir os mesmos arquivos do primeiro — 20<img>na dobra, 5 arquivos distintos por esteira. Se a dobra vive no fim de uma página longa, o custo é adiantado, e é esse o preço de uma esteira que anda por transform. -
A largura do conjunto é medida com
getBoundingClientRect, e não comoffsetLeft, porqueoffsetLeftdevolve inteiro. Medido: o conjunto tem 2 275,16px e ooffsetLeftdiz 2 275 — 0,16px a menos do que o-50%da animação de CSS usa. Não acumula, porque a volta reposiciona a cada laço, mas é precisão de graça. E não há remedição noloaddas imagens de propósito: o item tem largura fixa (flex-basisem--flux-card) e o quadro temaspect-ratio, então a fila já mede o mesmo antes de qualquer foto chegar; a legenda é absoluta, então nem a troca de fonte mexe na conta. Isso é o caminho certo, e não o remendo — remedir noloadseria consertar um layout que não deveria depender da imagem. -
O vão entre conjuntos é
paddingdo conjunto, nãogapdo trilho. Comgapno trilho, a largura total seria2×conjunto + 1 vão, o-50%cairia no lugar errado e o laço daria um solavanco por volta. Medido com o padding: metade do trilho = 2 275,16px, largura do conjunto = 2 275,16px — o mesmo número. -
O impulso é energia, não direção: rolar para cima acelera tanto quanto rolar para baixo, e as esteiras nunca trocam de lado. Medido: rolando para cima,
--flux-skewchega aos mesmos 6deg e a esteira de cima continua indo para a esquerda. A alternativa existe como variação, e foi descartada como padrão porque a inversão faz o olho perder o rastro da foto que estava seguindo — num par de fileiras cruzadas, isso lê como falha. -
Quem coalesce a rajada do trackpad é o laço do core, não uma linha do script.
LIB.onScrollroda os assinantes dentro de umrequestAnimationFrame, ou seja no máximo uma amostra por quadro: os eventos de trackpad e de roda inercial, que chegam com 2 a 4ms entre um e outro, viram uma medida só. Medir evento a evento devolveria picos de dezenas de milhares de px/s. Sobram duas travas no denominador: piso de 6ms (proteção) e teto de 200ms — é o teto que faz um salto de âncora depois de a página ficar parada ainda contar como movimento rápido, em vez de virar velocidade ridícula por causa do intervalo ocioso. O teto do impulso (--flux-boost:6) é a trava final: medido, o pico é de ±274px/s, 6,85× o cruzeiro. -
backdrop-filternos 20 véus não custou nada mensurável na linha principal. Medido com as fotos andando: 4,20ms de mediana de quadro com o desfoque e 4,20ms sem, inclusive com as fotos a 6° de inclinação. Quem desenha o desfoque é o compositor. A ressalva honesta: o medidor é headless e não enxerga taxa de preenchimento de GPU, então em máquina fraca com muitas instâncias na mesma página a conta pode ser outra — e aí a saída é apagar obackdrop-filterdo.flux-veil, que o gradiente sozinho já resolve a legibilidade. -
Véu e legenda são dois elementos, e não um só com padding. A máscara vale para o elemento inteiro, filhos incluídos: com a pílula dentro do véu, o topo dela caía junto com o degradê — 60% de opacidade em 390px, onde o véu é baixo. Separados (a mesma divisão do
conteudo/pilares-texto-na-foto), a máscara amacia só o véu e o texto fica cheio. -
A altura do véu é fração da foto, não padding em px. Com padding fixo de 56px, o mesmo véu que ocupava 35% de uma foto de 270px ocupava 52% de uma de 156px — em 390px, metade da imagem sumia sob o degradê. Em
--flux-veil:38%, a proporção é a mesma em qualquer largura. -
A legenda tem 24 caracteres de teto, e o número é medido. Em 390px a foto tem 249,6px e a pílula cabe em 225,6px de vão útil. Com 30 caracteres ("Sala de reunião — Porto Alegre") ela vazava 13,1px para fora do quadro; encurtada para "Sala de reunião — POA", sobram 52,8px. A rede é o par
overflow:hidden+text-overflow:ellipsisna pílula: passou do que cabe, a legenda corta com reticências em vez de ser decepada pelo recorte da foto — o aviso fica visível, e é de propósito. -
animation-name:nonee nãoanimation:noneno.is-driven. A abreviação zeraria também oanimation-direction:reverseda esteira de baixo, e no dia em que alguém tirar o.is-drivenpara depurar, ela voltaria andando para o lado errado — um sintoma que não tem nada a ver com a causa. -
Testar "o laço dorme fora da tela" exige tirar a dobra da tela de verdade.
IntersectionObservercomthreshold: 0reportaisIntersecting: truena tangência exata (rectTop === innerHeight), e a dobra tem 1 122px numa janela de 900px: num preview curto ela nunca sai. Com a página alta o bastante, o número é limpo — 0 escritas detransformem 90 quadros fora da tela, contra 180 (dois trilhos × 90 quadros) dentro dela. É por isso que os dois blocos de contexto do preview têmmin-height:112vh. -
Proposta para o core: a receita da esteira já aparece em seis componentes.
width:max-content+ dois conjuntos idênticos +@keyframesatétranslateX(-50%)está escrita, com nomes diferentes, emprova/marquee-logos,prova/carrossel-depoimentos,midia/carrossel-perfis(a esteira do rodapé),conteudo/trio-cards-ui-simulada,hero/duas-colunas-produtoe aqui — seis cópias, contra as três que o §6 do contrato dá como gatilho de promoção. Aqui ela não foi recopiada por preguiça: é o fallback sem JavaScript, e por isso precisa mesmo estar no arquivo. A promoção é decisão de arquitetura e não foi feita nesta pasta. Já a máscara horizontal das pontas está na segunda ocorrência (esta e a de depoimentos) — a ficha daquele componente pede a promoção na terceira, e ainda não é. -
A dobra nunca chega acelerada, e isso confunde quem vai testar. O impulso só se acumula enquanto a dobra está em cena: fora dela o laço dorme e o
boosté zerado na saída. Quem salta direto para a seção por âncora, ou quem rola a página inteira de uma vez, encontra as esteiras no ritmo de cruzeiro — medido:--flux-skewde 0deg na chegada, mesmo depois de um salto de 1 007px. É de propósito: energia acumulada por um movimento que ninguém viu acontecer faria a dobra abrir torta, sem causa visível. Rolando dentro da dobra, o mesmo gesto leva a inclinação aos 6deg em dez quadros. Ao medir isto, cuide para o gesto não empurrar a dobra para fora da tela no meio do caminho — foi o que aconteceu na primeira tentativa, e o zero que aparece no fim é o da saída, não o da falta de reação. -
--flux-skewsó é reescrita quando muda de verdade (limiar de 0,04deg). Custom property na raiz invalida o estilo de todos os descendentes que a leem — aqui, 20 quadros e 20 pílulas. Escrever o mesmo valor a cada quadro pagaria essa conta para nada, e em repouso a pagaria para sempre. -
Não há pausa no hover, e é decisão, não esquecimento. Isto é passagem, não leitura: não há texto para ler nem alvo para acertar, e parar a fileira sob o ponteiro só faria a dobra travar quando o mouse cruza a tela a caminho de outra coisa. Quem quiser a dobra parada tem
--flux-speed:0, que para as duas de uma vez.
Histórico
| Projeto | Onde | Data | Diferenças |
|---|---|---|---|
| autoral | construído do zero na biblioteca | 2026-09-03 | — |
Origem. Padrão de interação recorrente em site premium: a esteira de imagens cuja velocidade se acopla à rolagem, com inclinação proporcional à aceleração. Aparece em vitrines de estúdio premiadas no Awwwards, nos componentes de marquee que acompanham os templates de Framer, e nas faixas de produto que Apple e Nike usam em página de lançamento. Não há referência única e nada foi copiado: reescrito do zero dentro do contrato desta biblioteca. As implementações que se vê por aí são GSAP com ScrollTrigger e scrub, ou React com uma biblioteca de motion — nenhuma das duas caberia aqui, e a tradução para vanilla foi o trabalho.
Decisões de tradução para o contrato.
- Virou token: cor do véu (
--dark-rgb), texto e contorno da legenda sobre a foto (--light), canto da foto (--radius-media), fundo do quadro antes da foto pintar (--rule), curva das transições (--ease). Nenhuma cor crua no arquivo, fora as paradas demask-image, que são opacidade. - Virou variável do prefixo: os quatro parâmetros da reação (
--flux-speed,--flux-react,--flux-boost,--flux-skew-max) mais forma e ritmo (--flux-card,--flux-gap,--flux-space,--flux-veil,--flux-dur). Todos ficam na raiz do componente e são o único lugar onde se mexe. - Virou gancho do core: a entrada das duas esteiras e do texto é
data-rv/data-d/.ttl, sem observer próprio; a rolagem éLIB.onScroll, o laço único da página, e não um listener por componente. O fallback com listener passivo existe só para quem colar o componente sem omotion.js, e mantém o contrato que importa: uma amostra por quadro. - Virou primitiva do core reaproveitada: a legenda é a
.pill, com seletor duplo (.flux-card .flux-tag) para não depender da ordem em que as folhas foram coladas. - Acessibilidade que o padrão comum não tem: o segundo conjunto de cada esteira sai com
aria-hidden="true"ealt=""— o leitor de tela ouve cinco fotos por esteira, não dez —, nenhum elemento focável dentro das fileiras,althonesto em cada foto do primeiro conjunto, e movimento reduzido que para tudo por uma regra declarativa só, com o script se recusando a assumir. - Virou progressivo, o que o padrão de origem não é: GSAP não roda sem GSAP. Aqui as duas esteiras andam por animação de CSS, e o script só entra para melhorar — e entra lendo a matriz que a animação já produziu, para a troca custar 0,51px.
Divergências conhecidas vs. o padrão comum.
- O sentido nunca inverte. Quase toda implementação por aí faz a fileira andar para trás quando se rola para cima, porque é o que sai de graça de um
scrub. Aqui o impulso é o módulo da velocidade: a fileira acelera, mas não vira. A outra leitura está registrada como variação. - A velocidade de cruzeiro não é zero. Em
scrubpuro, parar de rolar para a esteira. Aqui ela continua andando a 40px/s e o scroll só acrescenta — uma dobra que congela ao parar lê como travada, e o argumento no meio dela deixa de ser lido. - A inclinação satura antes da velocidade. O tombo chega ao máximo em 5× a base, e a aceleração vai até 6×: o último terço do impulso é só velocidade. Sem isso, a foto continua tombando enquanto já está rápida demais para se ver o que é.