← Biblioteca componentes/midia/galeria-scroll-horizontal/ficha.md Abrir previewmarkupestiloscript
midia/galeria-scroll-horizontal

Galeria de cases em scroll horizontal

🔵 experimental complexidade média prefixo hgal- galeria carrossel scroll-horizontal portfolio cases imagem snap teclado

Uma faixa de cases que sangra até as duas bordas da página e rola para o lado, com encaixe (scroll-snap) e barra de rolagem escondida. Cada card é uma foto 4:3 em cinza parcial que ganha cor e um passo de zoom sob o ponteiro; no canto de cima aparece um distintivo redondo na cor de marca, e no rodapé da própria imagem ficam o nome e o tipo do trabalho. O cabeçalho traz título à esquerda e, à direita, a dica "Arraste para ver" com uma seta que cutuca a cada 2,2s — mais dois botões que fazem a faixa andar de card em card.

É a segunda entrada da categoria mídia e a primeira galeria horizontal da biblioteca.


Quando usar

Quando não usar

Contra os vizinhos

Precisa de Use
Coleção de fotos que valem juntas, sem destino próprio midia/galeria-pasta
Poucos itens, cada um com título e descrição listas/leque-paineis
Muitos itens com foto e nome, cada um com sua página este

galeria-pasta é uma pilha que abre; aqui nada abre — a faixa já está aberta e só anda. leque-paineis mostra um item por vez em largura variável; aqui todos os cards têm o mesmo tamanho e a hierarquia é a ordem, não o foco.


Padrão editorial

Slot Regra
Olho (.eyebrow) 2 a 4 palavras. Diz o recorte da lista: "Trabalhos selecionados", "Projetos 2026". A palavra "selecionados" é útil — avisa que a faixa não é o catálogo inteiro.
Título (.sec-title) 1 linha, uma palavra em <em>.
Dica (.hgal-hint) 3 a 4 palavras, imperativo. "Arraste para ver". Não escreva "role para o lado": é instrução de mecanismo, não convite.
Nome (.hgal-name) 1 a 3 palavras. O cliente ou o projeto. Nome comprido quebra em duas linhas e come a foto.
Tipo (.pill.hgal-type) 1 a 3 palavras, sempre do mesmo conjunto fechado em todos os cards: "Identidade visual", "Site institucional", "Rebrand". Se cada card inventa a própria categoria, a pílula deixa de classificar.
alt de cada foto Descreve a foto, não o case — o nome do cliente já está na legenda logo abaixo e seria anunciado duas vezes.
href de cada card Aponta para a página do case. Um card que leva a lugar nenhum é pior que um card a menos.

Quantidade: 4 a 8. Abaixo de 4 não há faixa. Acima de 8, quem chega ao fim rolou por mais de duas telas e meia sem um índice — e a faixa vira um túnel.

Escreva os nomes juntos. A legenda é o único texto do card, e nomes de comprimentos muito diferentes fazem a linha de base dançar de um card para o outro.


Especificação das imagens

Proporção paisagem, recortada em 4:3 pelo object-fit:cover
Mínimo 1600×1200 — o card chega a 460px de largura e a foto ainda faz scale(1.06) no hover
Recorte o assunto tem de sobreviver a corte nas laterais: 16:9 entrando numa caixa 4:3 perde cerca de um quarto da largura
Peso até 220 KB cada. São 4 a 8 na mesma dobra
Tom contraste médio. A foto entra em grayscale(.3): escolha imagens que funcionem pela forma, porque a cor só aparece inteira sob o ponteiro
Base os últimos 25% da altura ficam sob o véu da legenda. Não deixe o assunto ali

Por que ph-wide-* no preview. A caixa é 4:3 paisagem. Os ph-wide-* são 16:9 e perdem as laterais no recorte — perda pequena e realista. Os ph-* são retrato 3:4 e perderiam mais da metade da altura para caber em 4:3: o preview mostraria um recorte que ninguém entregaria de verdade. São cinco ph-wide-* na pasta de demonstração, e por isso o padrão tem cinco cards — repetir foto num portfólio lê como erro, não como placeholder.


Como usar

Cole markup.html, estilo.css e script.js. Depende do core (tokens, base, motion) e de mais nada.

A sangria — como o trilho sai da coluna

O contrato (§4) proíbe margem negativa e proíbe mexer na coluna. A solução aqui tem duas partes:

  1. O trilho é irmão do .wrap, não filho. O cabeçalho fica dentro do .wrap e recebe a coluna; o <ul class="hgal-track"> é filho direto da <section> e por isso já nasce com a largura da página. Nada precisa ser puxado para fora de lugar nenhum.

  2. O primeiro card se alinha à coluna por um recuo próprio, não por margem:

--hgal-inset:max(var(--pad),calc((100% - var(--max)) / 2 + var(--pad)));

É a mesma conta que o .wrap faz — metade da sobra da página, mais o respiro lateral — só que aplicada como padding-inline do trilho. Em tela estreita a sobra é negativa e o max() devolve --pad, que é exatamente onde a coluna encosta. O trilho lê os mesmos --pad e --max do tema: mudou a coluna, a faixa acompanha sozinha.

O mesmo valor vai em scroll-padding-inline, senão o encaixe alinha os cards na borda da tela e não na da coluna — e o scrollIntoView do script erra pelo mesmo tanto.

O 100% da conta é a largura da seção. Se o trilho for parar dentro de outro contêiner com respiro próprio, a conta passa a medir a caixa errada e o alinhamento quebra sem erro nenhum no console.

O acesso por teclado

O original não tinha nenhum: a faixa só andava com gesto de trackpad ou de toque. Quem usa mouse sem roda horizontal, teclado ou leitor de tela ficava no primeiro card — roda vertical não move contêiner que rola no eixo x, a página rola no lugar dele. São três camadas:

Camada O quê
Card focável de graça cada card é um <a> de verdade. Tab percorre a faixa, Enter abre, :focus-visible acende a foto igual ao hover — e o distintivo aparece junto
Setas e andam de card em card, Home e End vão às pontas. Cima e baixo não são capturados: quem aperta seta para baixo quer descer a página
Botões os dois discos do cabeçalho rolam um card por clique. São a única saída para quem navega só com mouse; somem quando não há o que rolar

O deslocamento é sempre scrollIntoView({inline:'start', block:'nearest'}), nunca scrollLeft na mão — assim quem decide onde o card para continua sendo o scroll-padding do CSS. behavior vira auto sob prefers-reduced-motion, lendo LIB.reduce do core. O block:'nearest' é o que impede a página inteira de saltar na vertical a cada seta.

Quando o foco entra num card por Tab, o script rola a faixa até ele em vez de deixar o navegador dar o pulo seco. Nas setas o foco vai com preventScroll:true justamente para o pulo não acontecer antes do deslizar.

Ganchos

Gancho O quê
[data-hgal] a raiz. O script aceita várias faixas na mesma página
[data-hgal-track] o <ul> que rola. Tem de ser filho direto da seção
[data-hgal-nav] dica e botões. O script põe [hidden] quando não há o que rolar
[data-hgal-prev] / [data-hgal-next] os dois discos. disabled em cada ponta
--hgal-card largura do card
--hgal-gap vão entre cards
--hgal-dur ritmo do hover da foto (.6s)
--hgal-inset o recuo da sangria. Só mexa se souber o que a conta faz

Variações

Variação Como
Largura do card --hgal-card no .hgal. Sempre deixe um pedaço do próximo à mostra
Sem pílula de tipo apague o <span class="pill hgal-type">
Grão sobre a foto <span class="grain"></span> dentro de .hgal-media — a primitiva do core já vem posicionada
Em seção clara troque .sec-dark por .sec-paper. O card não muda: o texto sobre a foto lê --light e o véu lê --dark-rgb, os dois independentes do escopo de tom
Cards que não são link troque <a> por <button> e tire o distintivo de seta. Não use <div>: o teclado inteiro depende do elemento ser focável por natureza

Gotchas

Como confirmar sem se enganar, pelo console do painel:

js const t = document.querySelector('.hgal-track'); t.querySelectorAll('.hgal-card')[1] .scrollIntoView({behavior:'auto', inline:'start', block:'nearest'}); t.scrollLeft; // > 0 significa que a lógica está certa

Se 'auto' rola e 'smooth' não, é o ambiente. A mesma armadilha atinge qualquer componente que dependa de rolagem suave.


Histórico

Projeto Onde Data O quê
Gabriel Rocha Studio gabriel-rocha-studio-2.html #trabalhos 2026-08 Origem. Site autoral, rodou uma vez. Classes sem prefixo (.case, .ph, .name, .type), cores cruas, entrada por GSAP, sem acesso por teclado.

O que mudou na extração:

No original Aqui Por quê
.case, .ph, .name, .type, .work-* tudo sob hgal- .name e .type colidem no primeiro projeto que juntar dois componentes
.case:hover{border-color:#3a3a36} --rule-2 é literalmente o que --rule-2 significa: um degrau neutro acima da linha de repouso
rgba(47,107,255,.9) no distintivo color-mix(in srgb,var(--accent) 90%,transparent) azul de um projeto só. O ícone passou a ler --dark, a mesma dupla do .btn-primary
rgba(6,7,10,.25) no véu rgba(var(--dark-rgb),…) mesmo motivo, e o véu virou o da legenda
Nome e tipo numa tarja abaixo da foto legenda dentro da imagem, com gradiente e blur mascarados contrato §6, texto sobre imagem. De quebra o card virou uma imagem só: nome curto e nome longo não desalinham mais entre cards
Tipo em texto solto à direita primitiva .pill do core, retonalizada para a ilha escura §6 — a biblioteca já tem rótulo em pílula
gsap.from('.case',{opacity:0,x:42,stagger:.1}) data-rv + data-d do core o contrato não aceita lib para entrada de elemento. Perdeu-se o deslize lateral; ganhou-se o blur de entrada que a biblioteca inteira usa
Tilt 3D por mousemove + GSAP nada rotação 3D presa ao ponteiro em card dentro de contêiner que rola é caro e briga com o encaixe. Se voltar um dia, é variação com nome, não padrão
@keyframes nudge global @keyframes hgalNudge nome de keyframe é global: nudge colide com o próximo componente que cutucar alguma coisa
animation:nudge 2.2s ease var(--ease) §3 proíbe curva escrita na mão. O vaivém ficou um tico mais seco
padding:0 var(--pad) clamp(50px,8vh,80px) no trilho .sec-pad na seção + recuo calculado §4: ritmo vertical é --sec-gap, não clamp solto. E o recuo lateral passou a acompanhar --max
border-radius:16px cravado --radius-media projeto flat entrega 0, projeto arredondado entrega o raio dele
Sem teclado <a> focável, setas, Home/End, dois botões mecanismo de gesto sozinho exclui mouse sem roda horizontal, teclado e leitor de tela
<a> soltos num <div> <ul> / <li> o leitor de tela anuncia "5 itens" antes do primeiro card — a informação que o scroll horizontal esconde
Fotos injetadas por JS de uma lista externa <img> no markup, placeholders locais dependência externa é proibida, e imagem que só existe depois do script não tem alt nem entra no HTML

Status experimental: rodou uma vez, num site autoral, e nesta forma normalizada ainda não rodou em lugar nenhum. Espere ajuste no primeiro uso real — principalmente na largura do card e na quantidade, que são as duas decisões que dependem do acervo de fotos do cliente. Rodando num projeto, vira estavel e esta tabela ganha a linha dele.