A seção prende a tela e a página continua rolando: enquanto a rolagem vertical desce, uma fila de fotos grandes e pequenas atravessa o palco da direita para a esquerda, um pixel para cada pixel. O cabeçalho — olho e título curto — fica parado no canto de cima, flutuando sobre as imagens; no pé, uma barra de progresso e um contador 03 / 06 dizem onde a pessoa está. Cada card carrega o número da etapa em corpo grande no canto e, dentro da própria imagem, o nome e uma linha. Quando o último card encosta na borda direita da coluna, o palco solta e a página segue.
O problema editorial é a sequência longa que não cabe empilhada. Seis etapas de um método viram, numa lista vertical, seis telas de rolagem em que cada item chega sozinho e o conjunto nunca é visto; viram, numa grade, seis cards pequenos em que a ordem some. Aqui a ordem é o próprio eixo do movimento — o que vem depois está literalmente à direita —, e a alternância de larguras dá à fila um ritmo de exposição, não de tabela. É o padrão que sites premiados usam para linha do tempo, método e portfólio, e que aqui roda sem GSAP: a conta é do componente, dentro do laço único do core.
Quando usar
- Sequência de 5 a 7 etapas em que a ordem importa e cada etapa tem uma imagem que a representa: método, jornada do cliente, linha do tempo, fases de uma obra.
- Quando a dobra pode ocupar a página inteira por três ou quatro telas de rolagem. Ela é uma parada, não um item de uma lista de dobras.
- Quando existe foto boa para todas as etapas. Uma etapa sem imagem à altura derruba a fila inteira: aqui não há card de texto.
- No meio da página, depois de a pessoa já ter entendido do que se trata. A faixa cobra atenção contínua e recompensa quem já está dentro.
Quando não usar
- Acima da dobra. Prender a tela antes de a pessoa saber onde está lê como página travada.
- Menos de quatro itens. Sem fila longa não há travessia: use
midia/galeria-scroll-horizontal, que sangra e rola sem sequestrar a rolagem da página. - Quando cada item precisa de um parágrafo. Cabem um nome e uma linha dentro da foto. Item com descrição é
listas/leque-paineis; etapa com texto de verdade élistas/baralho-sticky. - Quando os itens precisam ser comparados. Tudo que sai da tela some; comparação exige ver junto.
- Duas vezes na mesma página. Duas seções que prendem a tela transformam a página num carrossel de telas cheias, e a segunda cobra o dobro de paciência.
Contra os vizinhos
| Precisa de | Use |
|---|---|
| Etapas com título e parágrafo, sobre uma foto pinada | listas/baralho-sticky |
| Vitrine de cases com destino próprio, sem prender a página | midia/galeria-scroll-horizontal |
| Fila de pessoas que anda sozinha e responde ao arrasto | midia/carrossel-perfis |
| Três a quatro blocos de imagem com número e frase, parados | conteudo/pilares-texto-na-foto |
| Sequência longa de fotos em que a ordem é o assunto | este |
O baralho-sticky é o irmão mais próximo: os dois prendem uma tela e os dois numeram etapas. A diferença é o que a rolagem produz — lá ela empilha cards de texto sobre uma foto de fundo; aqui ela move imagens, e o texto é legenda. Se a etapa se explica por escrito, é lá. Se ela se mostra, é aqui.
Padrão editorial
| Slot | Regra |
|---|---|
Olho (.eyebrow) |
1 a 2 palavras. Nomeia a sequência: "Método", "Trajetória", "Como funciona". |
Título (.sec-title) |
Uma linha só, até 32 caracteres, com uma palavra em <em>. Duas linhas levam o cabeçalho de 136px a 172px de altura e comem a folga que separa o texto do card em tela baixa (ver gotchas). |
Número (.pin-num) |
Sempre dois dígitos, 01 a 09. É a única marca de ordem dentro da imagem, e o zero à esquerda é o que a faz ler como série. |
Nome (.pin-name) |
1 a 3 palavras, substantivo. "Diagnóstico", "Política de investimentos". Nomes com verbo ("Fazemos o diagnóstico") quebram o paralelismo da fila. |
Linha (.pin-line) |
40 a 60 caracteres, sem ponto final. É uma legenda, não uma frase de venda: diz o que aquela etapa entrega, em concreto — "O documento que orienta cada decisão", "Comitê trimestral e relatório no dia 5". Acima de 60 caracteres ela vira três linhas no card pequeno e o véu sobe demais sobre a foto. |
Contador (.pin-count-all) |
O total, escrito à mão no markup, com dois dígitos. Trocar o número de cards sem trocar este é o erro mais fácil de cometer aqui. |
alt de cada foto |
Descreve a foto, não a etapa — o nome já está na legenda e seria anunciado duas vezes. |
Quantidade: 5 a 7. O comprimento do trecho preso é a soma das larguras: seis cards alternados pedem cerca de 3,5 telas de rolagem em 1440×900 (medido: 3.135px de trilho sobre uma seção de 4.035px). Oito passam de cinco telas, e a partir daí a pessoa que não se interessou pela terceira etapa não tem como sair sem rolar muito.
Alterne as larguras em pares. Grande, pequeno, grande, pequeno. É a alternância que faz a fila ler como exposição; seis cards iguais leem como régua, e aí o componente certo provavelmente é outro.
Escreva as seis linhas juntas, na mesma sessão. Elas passam uma depois da outra sem nada entre elas: qualquer diferença de tamanho ou de tom entre a primeira e a quarta aparece.
Especificação dos assets
| Formato | paisagem, recortada em 16:10 pelo object-fit:cover (4:3 abaixo de 760px) |
| Mínimo | 1600×1000. O card grande chega a 835px de largura em 1440, e a 1.170px em tela de 2.000 |
| Peso | até 240 KB cada; são 6 na mesma dobra, e três delas carregam antes de aparecer |
| Recorte | o assunto tem de sobreviver a corte nas laterais e a duas caixas diferentes: a mesma foto roda em 835×522 e, no celular, em 328×246 |
| Tom | contraste médio, sem branco estourado no canto superior esquerdo (o número 01 mora lá) nem no terço de baixo (o véu da legenda) |
| Base | os últimos 30% da altura ficam sob o véu. Nada de rosto ou assunto ali |
| Não pode ter | texto na imagem, logo de instituição real, marca d'água |
Por que ph-wide-* no preview, e por que uma foto se repete. A caixa é paisagem; retrato entrando nela perderia mais da metade da altura e o preview mentiria sobre o enquadramento. Existem cinco ph-wide-* na pasta de demonstração e o padrão tem seis cards, então ph-wide-4 aparece em 02 e em 06. Aqui a repetição é aceitável — as duas etapas são de comitê e documento, e os cards estão a quatro posições de distância —, mas num site de verdade ela lê como falta de material: seis etapas pedem seis fotos.
Como usar
<link rel="stylesheet" href="componentes/midia/faixa-horizontal-pinada/estilo.css">
<script src="componentes/midia/faixa-horizontal-pinada/script.js"></script>
O script tem de ficar no fim do <body> (ou com defer): é ele que troca o modo, e a troca só é invisível se acontecer antes da primeira pintura. Medido no preview: zero entradas de layout-shift.
Ganchos
| Gancho | Papel |
|---|---|
[data-pin] |
a seção. O script parte daqui e escreve nela as três variáveis de estado |
[data-pin-track] |
o <ul>. Anda por transform no modo pinado; rola sozinho no nativo |
[data-pin-count] |
o <span> do número atual — o único texto que o script reescreve |
.pin-item--wide |
card grande (58vw). Sem a classe, o card é o pequeno (36vw) |
.is-pinned |
posta pelo script quando o modo pinado vale. O CSS nasce no modo nativo |
.is-near |
dobra por perto; só enquanto está lá o trilho pede camada de GPU |
--pin-len |
px que o trilho sobra da tela. Escrita pelo script; dá altura à seção |
--pin-x / --pin-p |
quanto o trilho andou (px) e o progresso (0 a 1) da barra |
--pin-lerp |
suavização, .14 no padrão. 0 gruda no scroll |
--pin-gap |
vão entre cards, clamp(18px,2.4vw,34px) |
--pin-band |
faixa reservada no topo e no pé, clamp(140px,15vh,168px). É o teto de altura do card |
--pin-inset |
recuo que refaz a conta do .wrap e alinha o primeiro e o último card à coluna |
--pin-vh |
altura do palco: 100svh onde existe, 100vh de reserva |
Variações
- Sem suavização —
--pin-lerp:0. O trilho gruda no scroll, sem perseguição. Mais seco e mais preciso; é o que se quer em telão e em vídeo gravado quadro a quadro. - Todos os cards do mesmo tamanho — apague
.pin-item--widede todos os<li>. O trecho preso cai quase pela metade e a fila vira catálogo. - Mais ou menos etapas — cada card acrescenta a própria largura ao trecho preso; atualize o total do contador à mão.
- Cards como link — troque
<figure>por<a class="pin-card" href>. Exige tratar o foco (ver gotchas). Ainda não rodou. - Em seção clara — troque
.sec-deeppor.sec-paper. O que está dentro da foto lê--lighte o véu lê--dark-rgb: os dois independem do escopo de tom.
Gotchas
-
O modo pinado é opt-in, e isso é o contrário do que parece certo. O CSS nasce em modo nativo (faixa de
scroll-snap) e o script acrescenta.is-pinned. Se fosse ao contrário, JS bloqueado ou quebrado deixaria uma seção de uma tela comoverflow:hiddenescondendo quatro dos seis cards para sempre. O preço é uma troca de layout no carregamento — medida como zero de layout shift, porque o script roda antes da primeira pintura. Se alguém movê-lo para o<head>semdefer, ou carregá-lo tarde, a troca passa a ser visível. -
A seção não pode ter
overflow:hidden, por mais que o trilho vaze dela.overflowdiferente devisibleem qualquer ancestral prende oposition:stickya um scrollport que não rola. Medido: com.pin{overflow:hidden}, no meio da dobra o topo do palco vai de0para −1568px — ele para de grudar e desce junto com a seção. Quem recorta é o.pin-stage, que é o próprio elemento que gruda. (Obody{overflow-x:hidden}do core não atrapalha: quem rola continua sendo o documento.) -
overflow-x:visiblecomoverflow-y:hiddennão é visible. Quando um eixo évisiblee o outro não, o navegador promove ovisibleaauto— medido no computado. O trilho virava contêiner de rolagem por baixo do pano, comscrollLeftpróprio brigando com otransforme um pulo lateral toda vez que o foco entrava nele. No modo pinado os dois eixos sãovisible. -
scrollWidthmede errado o trilho — e o erro é exatamente o recuo. Comoverflow:visible, oscrollWidthdo trilho dá 4.395px onde o conteúdo real tem 4.560px em 1440: os 165px que faltam são opadding-inline-end, que nenhum motor conta aí. Usar esse número faria o último card parar 165px antes da borda da coluna, e o palco soltaria o sticky com a fila fora de lugar — o salto no fim. A medida honesta é a borda direita do último card menos a esquerda do trilho, mais o recuo; ela é imune aotransformporque as duas caixas andam juntas. Resultado: o último card para a 164,8px da coluna em 1440 (recuo 164,5) e a 76,3px em 1280 (recuo 76,5). O erro de 0,3px é oMath.rounddo--pin-len. -
A suavização não pode chegar às pontas. Com
--pin-lerp:.14o trilho persegue o alvo, e numa rolagem rápida o atraso chegaria a centenas de pixels — inclusive no último quadro do trecho preso, onde o palco solta e a fila sairia escorregando. O teto de atraso é a distância até a ponta mais próxima, limitada a 240px: no meio ele deixa o atraso trabalhar, em0e emlenele vale zero. Medido nas duas pontas:--pin-xexatamente0e exatamente3135px. -
A barra segue o trilho, não a rolagem. São números diferentes enquanto a suavização persegue o alvo, e uma barra adiantada denuncia o atraso em vez de escondê-lo. Por isso
--pin-pé--pin-x / --pin-len, e não o progresso do scroll. -
A largura do card tem teto de altura, e é isso que impede a colisão com o cabeçalho.
min(58vw, --pin-free * 1.6): numa caixa 16:10, um card dessa largura tem exatamente--pin-freede altura, e--pin-freeé a tela menos as duas faixas reservadas. Em 1440×900 manda ovw(835×522, com 189px de folga contra um cabeçalho de 136px); em 1440×700 manda a altura (672×420, folga de 140px contra 122px). Se o título ganhar uma segunda linha, o cabeçalho passa de 172px e a conta perde a folga — suba--pin-bandjunto, ou encurte o título. -
100svh, não100vhnem100dvh. Em tablet com barra de endereço que some,vhmuda de valor no meio do gesto e o sticky salta;dvhmuda a cada quadro.svhé a menor das alturas e não se mexe. O fallback vem de um@supports, e não de duas declarações da mesma custom property:--pin-vh:100vh; --pin-vh:100svhsempre elegeria a segunda, e num motor semsvha altura da seção virariaauto— a seção inteira desmontaria em silêncio. -
loading="lazy"num trilho horizontal chega tarde. O navegador só busca a foto quando otransforma traz para perto da borda direita. Medido: a imagem do card 5 ainda não estava carregada com o trilho em 10% e estava completa em 50% — exatamente o ponto em que aquele card entra na tela. Em rede lenta isso é um card entrando sem foto. Por isso os três primeiros sãoeager(estão na tela ou a uma batida de rolagem) e só do quarto em diante entralazy. -
A caixa 16:10 é o que segura o
--pin-len. A largura do card não depende da imagem: oaspect-ratioreserva a caixa antes de a foto chegar, e por isso a medida do primeiro quadro continua valendo depois. Oloadde cada imagem ainda pede uma remedida, mas como cinto e suspensório — se uma variação deixar a largura seguir a foto, a conta se refaz sozinha em vez de ficar errada em silêncio. -
Trocar de modo não pode deixar
transformpara trás. O deslocamento é--pin-xconsumido por uma regra que só existe em.is-pinned, e não umstyle.transformescrito no elemento. Assim a media query de 760px devolve a fila ao lugar sozinha, mesmo que o script ainda não tenha rodado oresize— e mesmo que alguém tenha coladoclass="pin is-pinned"à mão no HTML. -
O contador é
aria-live, então ele só pode ser escrito quando muda. Reescrever o mesmo número a 60 quadros por segundo faria o leitor de tela falar sem parar. Oaria-atomic="true"é o que faz a pessoa ouvir "03 / 06" inteiro em vez de um "03" solto. Mesmo assim, seis anúncios ao atravessar a dobra são seis interrupções: se a página tiver outra região viva perto, considere tirar oaria-live— a informação continua no DOM. -
O trilho é focável, e não os cards. Um
<a>por card exigiria que o Tab rolasse a página até o ponto em que aquele card está no centro (nãoscrollIntoView, que brigaria com otransform). Com umtabindex="0"no<ul>, o modo nativo ganha as setas do teclado de graça — medido:ArrowRightno trilho focado avança um encaixe inteiro, de 0 para 869px, e o contador vai a02. No modo pinado as setas rolam a página, que é o que move a fila. O anel de foco usaoutline-offset:-4pxporque o do core sai 3px para fora e ooverflow:hiddendo palco o comeria. -
will-changesó enquanto a dobra está por perto. O trilho pinado mede 4.395×522px em 1440; promovido para sempre, é textura de GPU parada na memória a página inteira. Enquanto está aceso, porém, ele se paga: o cabeçalho e o pé ficam em outra camada e param de ser repintados a cada quadro. Quem acende e apaga é oIntersectionObserver, pela classe.is-near. -
Escrever
--pin-lenmuda a altura da seção, e isso é seguro. A altura nova muda oscrollHeightdo documento, mas não muda o topo da seção nem a largura do trilho — não há laço. A única leitura depois de escrita no medidor é intencional: ooffsetHeightda seção logo em seguida é justamente o valor novo que se quer. -
html{scroll-behavior:smooth}do core vale para âncoras, não para a faixa. Um link#metododesliza suavemente até o topo da seção, que é onde o trecho preso começa — o comportamento certo. Já quem for medir este componente por script precisa rolar combehavior:'instant': com o suave, a medida sai no meio do caminho.
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 premiado e em página de produto: a seção que prende a tela e transforma a rolagem vertical em travessia horizontal. Aparece na família Awwwards/FWA, em templates de Framer e Webflow, e nas páginas de produto da Apple, que o usam para atravessar uma linha de tempo. Não há referência única, e não há uma linha copiada: nessas páginas o mecanismo é sempre GSAP ScrollTrigger com pin e scrub, que a stack fixa do contrato não aceita. Nada foi copiado: reescrito do zero dentro do contrato desta biblioteca.
Decisões de tradução para o contrato. O pin do ScrollTrigger virou position:sticky num palco de uma tela, com a altura da seção calculada a partir de --pin-len — sem elemento espaçador e sem position:fixed, os dois recursos que o plugin usa e que sujariam o markup. O scrub virou --pin-lerp, uma aproximação exponencial por tempo (a mesma perseguição em 60Hz e em 120Hz), com um teto de atraso que fecha nas pontas — o scrub do plugin não tem isso, e é por isso que a versão dele às vezes solta o pin com a fila ainda andando. O laço de scroll é o LIB.onScroll do core, um rAF para a página inteira, com fallback próprio para quem colar o componente sem motion.js. Cor, fonte, raio e o respiro da seção são token; o vão, a faixa reservada, as duas larguras de card e a suavização são variáveis do prefixo. A entrada dos dois primeiros cards é data-rv do core, nunca observer próprio.
O que o padrão comum quase nunca traz e aqui é obrigatório: um modo nativo que é o padrão do CSS — sem JS, em tela estreita ou com "reduzir movimento", a mesma dobra é uma faixa de scroll-snap horizontal, com o mesmo mecanismo do midia/galeria-scroll-horizontal, em vez de uma seção quebrada; contador em aria-live; trilho focável com setas do teclado; e o texto de cada card dentro da imagem, sobre véu mascarado, como manda o §6.
Divergências conhecidas vs. produção: nenhuma — o componente ainda não rodou num site. Espere ajuste no primeiro uso real em dois pontos: o comprimento do trecho preso (que depende do número de etapas e é a decisão mais fácil de errar) e a altura do cabeçalho, que é o piso de --pin-band.