Uma pasta fechada com uma pilha de fotos saindo por cima. O ponteiro por perto abre a pilha em leque e tomba a aba da frente; o clique estende as fotos em linha e tira a pasta de cena; arrastar qualquer foto para baixo fecha tudo de volta.
É o primeiro componente da categoria mídia, e o primeiro de origem externa da biblioteca.
Quando usar
- Galeria institucional que não é o assunto da página: bastidores, escritório, time, um ano de trabalho. A pasta fechada ocupa pouco e só cresce se a pessoa quiser.
- Quando as fotos valem juntas, como coleção, e nenhuma delas precisa aparecer sozinha em tamanho grande.
- Entre duas dobras densas de texto, como respiro — a metáfora da pasta lê rápido e não exige legenda para ser entendida.
Quando não usar
- Quando a foto é o conteúdo principal. Aqui ela abre em 224×288 e nunca em tela cheia. Portfólio de fotografia pede outra coisa.
- Quando as imagens precisam ser lidas em sequência ou comparadas lado a lado — o leque sobrepõe de propósito.
- Acima da dobra. O componente depende de interação para revelar qualquer coisa, e no primeiro contato ele é só uma pasta.
- Em página cujo público navega majoritariamente por teclado ou leitor de tela: funciona (veja os ganchos), mas o prazer do componente está no arraste, e esse prazer não se traduz.
Padrão editorial
| Slot | Regra |
|---|---|
Olho (.eyebrow) |
1 a 2 palavras. O tipo de coleção: "Bastidores", "Arquivo", "2026". |
Título (.sec-title) |
1 linha, uma palavra em <em>. Nomeia o lugar ou o período, não a ação: "Onde o trabalho acontece". |
Apoio (.sec-lead) |
1 parágrafo, 20 a 35 palavras. Situa o que a pasta guarda. A atenção é da imagem — não escreva três linhas aqui. |
Rótulo da pasta (.fold-label) |
2 a 4 palavras, formato de nome de arquivo: Bastidores.2026. É o que faz a metáfora fechar. Evite frase. |
Legenda (.fold-cap) |
2 a 4 palavras por foto, minúsculas viram caixa alta no CSS. Diz onde, não o que se vê: "Corredor norte", não "Um corredor bonito". |
alt de cada foto |
Obrigatório e descritivo. É o único conteúdo que chega a quem não vê a imagem. |
Quantidade: use ímpar. O --i vai de -n a n com o centro em 0. Com número par não existe centro, e o leque fica assimétrico sem parecer intencional. Cinco é o padrão; três funciona; sete começa a apertar no desktop.
Especificação das imagens
| Proporção | retrato, ~3:4 (a caixa é 224×288) |
| Mínimo | 900×1200, para aguentar o scale(1.05) da abertura |
| Recorte | object-fit:cover — o assunto tem de sobreviver a corte nas laterais |
| Peso | as 5 carregam de uma vez, sem lazy: mantenha cada uma abaixo de 200 KB |
| Tom | fotos escuras ou de contraste médio. Foto clara demais briga com a pasta e some a legenda |
Não há loading="lazy" de propósito: as fotos estão na pilha desde o início, e uma foto chegando depois do hover apareceria em branco no meio do leque.
Como usar
Cole markup.html, estilo.css e script.js. Depende do core (tokens, base, motion) e de mais nada.
Cada <li class="fold-item"> declara dois números no style:
<li class="fold-item" style="--i:-2;--a:2">
--i é a posição em relação ao centro, negativa à esquerda. --a é o mesmo em módulo, e serve só para a escala da pilha. Trocou a quantidade de fotos, refaça a sequência: com 5 é -2 -1 0 1 2, com 3 é -1 0 1.
Ganchos
| Gancho | O quê |
|---|---|
[data-fold] |
a raiz. O script aceita várias na mesma página |
.fold.is-open |
estado aberto, posto pelo script |
[data-fold-open] |
a aba da frente, um <button> de verdade, com aria-expanded |
[data-fold-close] |
saída por teclado; fica fora do fluxo enquanto fechada |
--fold-spread |
espalhamento da linha aberta, no .fold |
--dx / --dy / --dr |
canal do arraste: o script escreve, o CSS recompõe o transform |
Variações
| Variação | Como |
|---|---|
| Quantidade de fotos | refaça --i/--a. Prefira ímpar |
| Sem legenda | apague .fold-cap |
| Espalhamento | --fold-spread no .fold: 132px, 74px e 46px nos breakpoints |
| Ritmo | --fold-dur (.42s) e --fold-dur-lento (.6s) no .fold |
Gotchas
-
Use quantidade ímpar. Com par não existe
--i:0, o centro cai entre duas fotos e o leque fica torto sem parecer decisão. -
O
z-indexsobe com--i.calc(10 + var(--i))faz a foto da direita ficar por cima. Se você inverter a ordem no HTML sem refazer o--i, a sobreposição inverte junto e o leque lê ao contrário. -
A perspectiva mora no
.fold-stage, não no.fold. A aba tomba comrotateX; semperspectiveno pai imediato ela achata e o efeito some sem erro nenhum no console. -
O arraste não existe sob
prefers-reduced-motion. É deliberado: arrastar é movimento contínuo preso ao ponteiro, exatamente o que a preferência pede para evitar. Quem reduz movimento fecha pelo botão ou pela seta para baixo. Se você tirar essa guarda, tire também a promessa de acessibilidade da ficha. -
--dx/--dy/--drsão o único contrato entre script e estilo. O script nunca escrevetransform. Se você mudar a fórmula da posição aberta no CSS, o arraste continua funcionando sozinho — foi para isso que o canal existe. Se escrevertransformno script, quebra os dois. -
Fechar por clique fora usa
pointerdownnodocument. É um listener global por componente. Numa página com muitas pastas isso multiplica; se um dia forem muitas, troque por um listener único delegando para todas. -
A foto não abre em tamanho grande. Não há lightbox. Se a pessoa quiser ver a foto de verdade, não tem para onde ir — considere isso antes de usar com imagem que mereça leitura.
-
Sem
loading="lazy"de propósito. As cinco carregam juntas. Uma foto chegando depois do hover apareceria em branco no meio do leque. -
O
scale(1.05)da abertura pede reserva de resolução. Foto no tamanho exato da caixa (224×288) abre borrada.
Histórico
| Origem | Onde | Quando | O quê |
|---|---|---|---|
| 21st.dev | interactive-folder-gallery.tsx, por alexperezcedeno |
2026-08 | Origem do conceito. React + Tailwind + framer-motion, sob licença MIT do registro do 21st.dev. |
Reimplementado do zero, não adaptado: o original é incompatível com o contrato desta biblioteca em cinco pontos de uma vez (React, TypeScript, Tailwind, shadcn e framer-motion). O que veio de lá é a ideia — pasta, leque, arraste para fechar. O código é outro.
O que mudou na travessia:
| No original | Aqui | Por quê |
|---|---|---|
framer-motion com spring |
transition CSS com a curva --ease do projeto |
O contrato não aceita lib. A curva não é spring de verdade: o retorno não tem oscilação |
| Posições calculadas em JS por índice | --i/--a no markup, fórmula no CSS |
O leque de hover passa a funcionar sem JS nenhum |
<div> clicável |
<button> com aria-expanded |
Foco, Enter e Espaço de graça; o estado é anunciado |
| Fechava só por arraste | Arraste, Esc, seta para baixo, botão e clique fora |
Arraste é gesto de ponteiro; sozinho, exclui teclado |
| Sem tratamento de movimento | Arraste desligado sob reduce, leque vira deslocamento simples |
Exigência do contrato |
| 5 fotos do Unsplash por URL | placeholders locais | Dependência externa é proibida, e a CSP do site bloquearia |
Cores cruas (#1e1e1e, #2a2a2a) |
--dark, --dark-2, --rule, --rule-2 |
O mesmo componente tem de rodar em qualquer tema |
Status externo: nunca rodou em produção. Foi normalizado ao contrato e testado no preview, mas não tem quilometragem — espere ajuste no primeiro uso real. Rodando num site, vira estavel e esta tabela ganha a linha do projeto.