Seis linhas grandes atravessando a coluna, separadas por filete: número, título em display, categoria em pílula e uma seta. Ao passar o ponteiro, a linha sob a mão acende e as outras esmaecem para 35%; uma foto de retrato aparece desfocada, entra em foco e passa a acompanhar o cursor com atraso, inclinando-se de leve conforme a velocidade da mão. Trocar de linha troca a foto em fusão cruzada, sem piscar. Sair da lista devolve tudo ao repouso.
Resolve a dobra de "o que fazemos" quando existem seis frentes de trabalho e uma foto boa para cada uma, mas nenhuma delas merece um card. A grade de cards obriga a decidir o tamanho da foto antes de saber quantos itens existem, e seis cards de foto viram uma parede de imagem que compete com o título da seção. Aqui a lista lê como índice — texto, hierarquia, ordem — e a imagem só existe enquanto alguém está olhando para uma linha específica. É um carrossel de imagens dirigido pela mão, sem carrossel: não há seta, não há indicador e não há estado a navegar.
Quando usar
- Serviços, especialidades, frentes de trabalho, setores atendidos — de 4 a 8 linhas, cada uma levando a algum lugar (uma âncora, uma página).
- Quando existe uma foto por linha e as seis são do mesmo mundo visual. Fotos de origens diferentes viram um carrossel de amostras.
- Quando o texto de cada item cabe em três ou quatro palavras. O título é a peça gráfica da dobra; não há espaço para uma frase.
- Quando a página já tem imagem demais e uma dobra que respira faz falta: em repouso, esta seção é só tipografia sobre o escuro.
Quando não usar
- Quando cada item precisa de uma descrição junto do título. Use
listas/leque-paineis, onde o painel aberto mostra título e parágrafo. - Quando o conteúdo é pergunta e resposta:
listas/acordeao-faq. - Quando a foto é o assunto e o texto é legenda — aqui é o contrário. Para uma fileira em que a imagem manda,
midia/galeria-scroll-horizontalouconteudo/pilares-texto-na-foto. - Quando o clique tem de abrir o item em detalhe na mesma tela:
overlay/card-expansivel-fullscreen. - Quando o público é majoritariamente de celular. A dobra funciona no toque — cada linha mostra a própria miniatura —, mas ali ela é uma lista com miniatura, e o que a torna memorável não acontece. Se o desktop for minoria, escolha um componente cujo desenho principal seja o de toque.
- Com menos de quatro linhas: o esmaecimento das outras não tem o que contrastar e a dobra lê como três links soltos.
Padrão editorial
| Slot | Regra |
|---|---|
| Olho | Uma ou duas palavras. Categoria da dobra, não frase. |
| Título | Até 40 caracteres, com uma palavra em <em>. O .ttl do core cuida da entrada. |
| Apoio | Duas linhas, até 160 caracteres. Explica o critério da lista, não a lista. |
| Número | Dois dígitos com zero à esquerda (01). É régua visual, não ranking — não reordene por importância depois de publicar. |
| Título da linha | 14 a 26 caracteres. Acima disso o título encosta na pílula em 1280px e quebra em duas linhas em 1440px, desalinhando a fileira. |
| Categoria | Uma ou duas palavras, na .pill do core. Todas do mesmo tipo: ou todas são público (Família, Empresas), ou todas são produto (Offshore, Previdência) — misturar os dois eixos faz a coluna da direita parecer aleatória. |
| Foto | Uma por linha, retrato. Ver a especificação abaixo. |
O que mantém a dobra coesa é o paralelismo gramatical dos títulos: se um começa por substantivo ("Gestão de carteira"), todos começam. Um verbo no meio de seis substantivos salta mais do que parece, porque os seis são lidos em sequência e em corpo grande.
Especificação dos assets
- Formato e dimensão mínima: retrato, 900×1200 ou maior, WebP. O quadro exibido é 3/4, e o
object-fit:covercorta nas laterais. - Peso alvo: até 140 KB por foto. São seis, e todas entram em cache no
loadda página. - Composição: assunto no centro vertical e com folga nas bordas — a foto aparece pequena (230 a 310px de largura) e é vista de canto de olho, enquanto a pessoa lê a linha. Foto com detalhe fino não se lê nesse tamanho.
- Todas as seis com o mesmo tratamento de cor e a mesma temperatura. A fusão cruzada de 0,28s coloca duas delas na tela ao mesmo tempo, e diferença de tratamento aparece exatamente aí.
- O que não pode ter: texto embutido, logo, recorte em fundo branco no meio de fotos de ambiente, e rosto ocupando toda a moldura (a foto é pequena demais para retrato fechado funcionar).
Como usar
<link rel="stylesheet" href="componentes/listas/lista-imagem-flutuante/estilo.css">
<script src="componentes/listas/lista-imagem-flutuante/script.js"></script>
Ganchos
| Gancho | Papel |
|---|---|
[data-peek] |
Raiz. O script parte daqui e pendura a camada flutuante como filha direta dela. |
[data-peek-list] |
O <ul> que escuta ponteiro e foco, e recebe .is-hovering. |
data-peek-img |
Caminho da foto daquela linha. Vive no <a>, não num array do script. |
data-peek-alt |
Descrição da foto. Vai para o alt da imagem flutuante. |
--peek-w |
Largura da foto. clamp(230px,19vw,310px) no padrão; a altura sai do aspect-ratio:3/4. |
--peek-dx / --peek-dy |
Deslocamento a partir do cursor. 24px e -50% no padrão — a foto fica à direita e centrada na ponta do ponteiro. O y aceita porcentagem da própria foto; o script resolve px e % e mais nada. |
--peek-edge |
Margem mínima até a borda da janela, em px. 16px no padrão. |
--peek-lerp |
Fração do vão vencida por quadro a 60Hz. .16 no padrão; o script converte para a taxa real da tela. |
--peek-tilt |
Inclinação máxima, em graus. 8 no padrão — é teto de segurança, não o valor que se vê (ver gotchas). 0 desliga. |
--peek-row-pad / --peek-dim / --peek-dur |
Respiro da linha, opacidade das linhas apagadas e duração da troca de estado. |
--peek-x / --peek-y / --peek-rot |
Escritos pelo script a cada quadro. Não se ajustam à mão. |
.is-hovering |
Na lista: uma linha está em foco de atenção. O script põe no hover e no foco de teclado. |
.is-active |
Na linha acesa. |
.is-on |
Na camada flutuante: foto visível. |
.is-shown |
Na imagem da fusão que está acesa das duas. |
Variações
- sem inclinação —
--peek-tilt:0. A foto segue a mão sem girar. É a primeira a testar em marca conservadora. - foto colada no cursor —
--peek-lerp:.4. Acima de.6o efeito vira um cursor customizado; abaixo de.08a foto parece presa a um elástico. - foto à esquerda do cursor —
--peek-dxnegativo, já resolvido em px (o script não interpretacalc()). - foto em paisagem —
aspect-ratio:4/3no.peek-float, fotosph-wide-*e--peek-w:clamp(300px,26vw,420px). - em seção clara —
.sec-paperno lugar de.sec-deep. Número, título, filete e pílula acompanham o escopo; a foto não muda, porque ela é imagem sobre a janela e não sobre a seção. - lista mais curta ou mais longa — quatro é o piso. Acima de oito, a foto muda de linha mais rápido do que a fusão de 0,28s fecha.
Gotchas
- A foto flutuante não está no
markup.html, e isso não é esquecimento. Ela éposition:fixed, e qualquer ancestral comtransform,filter,perspective,contain:paintouwill-change:transforma transforma emabsolute. Medido nesta dobra: um<li class="peek-item">já revelado ([data-rv].in) reportafilter: blur(0px)— o core desliga o desfoque escrevendoblur(0), que ainda é um filtro. Ou seja, escrita dentro de um item da lista, a foto ficaria capturada para sempre, não só até o reveal terminar. O script a pendura como filha direta da<section>, que o core deixa composition:relativee nada mais. Não ponhadata-rvna<section class="peek">. - O
transformda camada de posição não temtransition, e não pode ter. A posição já é interpolação: olerpdo script produz uma nova coordenada por quadro. Umatransitionpor cima faria o navegador interpolar entre dois valores que já são resultado de interpolação, com a curva do--easeempilhada — a foto passaria a chegar em degraus de meio segundo. Comotransformé uma propriedade só, animar a escala junto obrigaria a transicionar também o translate. Por isso a escala mora numa caixa interna. - Não adianta escapar disso com a propriedade
scaleindividual. Ela é aplicada antes dotransformna conta da matriz (Transforms 2, Individual Transform Properties), então o resultado éS·T: a translação inteira sai multiplicada pelo fator da escala. Comscale(.9)e um deslocamento de 1200px, a foto erraria a posição em 120px enquanto a escala sobe. As três camadas (.peek-floatposição,.peek-float-boxentrada,.peek-float-imgtroca) existem por causa dessa conta. pointer-events:nonena camada é obrigatório. Uma foto de 300px encostada no cursor fica sob o ponteiro o tempo todo: sem isso ela rouba opointerenterda linha, a lista pisca entre ativa e inativa a cada quadro e opointerleavedispara dentro da própria lista.- Duas imagens, não uma com o
srctrocado. Reescrever osrcde um<img>já pintado apaga o quadro atual antes de o novo decodificar, e a troca de linha pisca em branco. O script escreve na imagem apagada, espera oloade só então troca qual das duas está acesa. As duas nascem com a foto da primeira linha: um<img>semsrcfica comnaturalWidth0 para sempre, e o primeiro hover teria de esperar um carregamento para acender qualquer coisa. - O teto de
--peek-tilté rede, não o valor que se vê. Medido com o padrão (--peek-lerp:.16): um golpe de mouse atravessando a janela inteira (1360px) chega a ±4,04°; um movimento de leitura (30px a cada 16ms) fica em 1,03°; parado, 0°. Quem limita ali é o amortecimento da própria inclinação (.34por quadro), não o clamp. Baixar--peek-tiltpara 3 corta o pico de verdade; subir acima de 6 não muda nada. - O
lerpé convertido para a taxa real de quadro, e isso é verificável. A foto cobre 85% do vão em 184ms, em qualquer tela. No medidor headless, que roda perto de 250fps, isso são 46 quadros; a 60Hz seriam 11. Sem a conversão (1 - (1-lerp)^(dt*60)), a mesma dobra andaria mais de quatro vezes mais rápido no headless — e o dobro num monitor de 120Hz. - O laço dorme. Medido: 30 amostras ao longo de ~500ms depois do
pointerleavee zero escritas em--peek-x. Ele para quando o vão cai abaixo de 0,3px, a inclinação abaixo de 0,05° e o ponteiro está fora da lista. Sem isso, uma página com esta dobra gastaria um quadro por vez para reescrever o mesmotransformaté a aba fechar. - O clamp usa
documentElement.clientWidth, nãoinnerWidth.innerWidthinclui a barra de rolagem, e a foto acabaria meio escondida atrás dela na borda direita. Medido em 1280×720: com o ponteiro no canto inferior direito, a caixa para exatamente emx=1264ey=704— os 16px de--peek-edge. Um detalhe do lado esquerdo: com--peek-dxpositivo, o limite da esquerda nunca dispara (com o cursor emx=2a foto já está em 26px), e quem inverter o--peek-dxpara pôr a foto à esquerda passa a exercitar o outro lado. - A inclinação faz a foto passar dos 16px de margem. O clamp é calculado sobre a caixa não girada. Com 4° e 310×413, os cantos avançam cerca de 14px além dela — a foto encosta na borda em vez de manter a margem. É aceitável porque a inclinação só existe durante o movimento rápido, e movimento rápido não termina no canto; se incomodar,
--peek-tilt:0. - O script sai inteiro em aparelho sem ponteiro fino, e a miniatura da linha não é fallback. É o desenho da versão de toque: sem hover não há o que perseguir, e o dedo cobriria a foto. Medido em 390×844: nenhum
.peek-floaté criado, as seis miniaturas de 64×80 aparecem e todas pintam. - No desktop as seis miniaturas nunca são baixadas.
display:nonemaisloading="lazy"fazem o navegador não pedir a imagem, e elas aparecem na lista de "imagens não pintadas" do medidor — o que está certo: no desktop quem carrega as fotos é o pré-carregamento do script, depois doloadda página. Em toque elas são renderizadas e as seis carregam normalmente. .is-hoveringtambém vale para o teclado. O nome guarda a origem do padrão, o papel é mais largo: quem chega por Tab acende a linha e apaga as outras exatamente como o ponteiro. Trocar o nome quebraria quem já leu a ficha; ler o nome ao pé da letra faria alguém "consertar" o comportamento do teclado.- No teclado a foto ancora, não persegue. Ela pousa encostada na borda direita da coluna, centrada na linha focada — medido em 1440px:
--peek-x962,5, que é1260,5 − 274 − 24, e a borda direita da foto cai exatamente sobre a borda da coluna. A medida é feita um quadro depois dofocusin: o navegador rola a página para trazer o link à vista depois de disparar o evento, e medir dentro dele devolveria a posição de antes da rolagem. Nada aqui move o foco, então não háfocus({preventScroll:true})a fazer — mas quem acrescentar setas para andar de linha em linha precisa das duas coisas juntas. - Rolar com uma linha focada reancora a foto, porque ela é
fixede a linha não. Isso assina oLIB.onScrolldo core em vez de registrar um listener de scroll próprio (contrato §7.4), e a assinatura é cancelada nofocusout. Semmotion.jsna página, o componente continua funcionando e só perde essa reancoragem. - O
altda miniatura é vazio de propósito. Ela está dentro de um link cujo texto já diz "Gestão de carteira", e repetir a descrição da foto ali só alonga o que o leitor de tela anuncia. A descrição honesta vive nodata-peek-alte vai para a imagem flutuante, que éaria-hidden— ela serve de texto de reposição se o arquivo faltar, não de conteúdo. - Abaixo de 760px a miniatura e a seta dividem a mesma célula da grade. Uma coluna para cada parecia mais arrumado e deixava o
column-gapda coluna escondida para trás — 14px de vão vazio colado na borda direita, medido em 390px. Dividir a célula é seguro porque o featurehovervale semprehoverounone, nunca os dois: uma das duas está sempre emdisplay:none. will-change:transformé permanente aqui, e é o caso para o qual a propriedade existe — otransformmuda a cada quadro. O preço é uma textura de ~310×413 (cerca de 510 KB de memória de GPU) enquanto a dobra existir. Em repouso a caixa de dentro fica emvisibility:hidden, então nada é pintado.
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 — o "list image hover" dos portfólios premiados no Awwwards, os templates de índice de projeto em Framer e Webflow, as páginas de serviço de estúdio de design. Não há referência única: a forma é conhecida o bastante para ser desenhada de memória, e foi. Nada foi copiado: reescrito do zero dentro do contrato desta biblioteca. A implementação corrente do padrão costuma vir com GSAP e um mousemove que escreve transform inline sem interpolação de tempo — aqui não entra nem uma coisa nem outra.
Qual irmão ele substitui, e em que caso (CLAUDE.md §11). Antes de construir, a busca no catálogo apontou listas/leque-paineis como o parente mais próximo: também é lista de serviços com foto e estado ativo. Ele resolve o caso em que cada item precisa de um parágrafo — o painel aberto mostra título e descrição, e a foto é o fundo do painel. Não resolve o caso desta dobra: seis painéis lado a lado espremem cada foto numa faixa de 90px, o título vira texto vertical e não sobra lugar para o número nem para a categoria. O leque é para quatro a seis serviços com explicação; esta lista é para seis frentes que se anunciam pelo nome e cuja foto é contexto, não conteúdo. Os outros dois vizinhos foram descartados por motivo mais simples: listas/acordeao-faq não tem imagem, e midia/galeria-scroll-horizontal inverte a hierarquia — lá a foto é o item e o texto é legenda.
Decisões de tradução para o contrato. Cor virou token: o título em repouso lê --fg-2 e acende em --fg, o número e a seta acendem em --accent, o filete é --rule, o canto da foto é --radius-media — a mesma dobra roda em .sec-deep e em .sec-paper sem tocar numa linha do componente. Os parafusos do efeito viraram variáveis do prefixo (--peek-w, --peek-dx/--peek-dy, --peek-lerp, --peek-tilt, --peek-edge) declaradas na raiz e lidas do CSS pelo script, para que ajustar o movimento seja mexer no estilo e não no código. A entrada da foto carrega opacidade, desfoque e escala como o §5 manda, e a posição ficou deliberadamente fora de qualquer transition. O lerp passou a ser calculado sobre o tempo do quadro, pela mesma disciplina do --pfl-speed em px/s do midia/carrossel-perfis. A entrada das linhas usa data-rv/data-d e .ttl do core, sem observer próprio.
E o padrão ganhou três coisas que a versão comum não tem. Teclado: chegar por Tab acende a linha e faz a foto pousar à direita dela, com reancoragem se a página rolar — na versão comum, quem navega por teclado nunca vê a imagem. Toque: cada linha traz a própria miniatura, e o script nem chega a rodar; a versão comum ou deixa a foto fora do celular ou a faz perseguir um dedo que não existe. Movimento reduzido: a foto não persegue nada — pousa na linha, pelo mesmo caminho de código do teclado. A alternativa óbvia (seguir o cursor "sem lerp") foi descartada porque seria mais movimento, não menos: a foto passaria a teleportar de posição a cada evento de ponteiro, dezenas de vezes por segundo, que é exatamente o que a preferência existe para suprimir.