Uma roda-gigante de cards com o cubo enterrado abaixo da dobra. O que aparece na tela é só a calota de cima do aro: o card do topo em pé, centrado e nítido; os vizinhos descendo pelas laterais, inclinados como raios de roda porque são tangentes ao arco, e esmaecendo à medida que mergulham. Arrastar de lado gira a roda, com inércia ao soltar e borracha nas duas pontas; ao parar, ela encaixa no card mais próximo do topo. Abaixo, um painel troca com blur: o ano em corpo grande, o título da parada e uma linha de texto. O olho segue o card que sobe, lê o painel, e volta ao arco.
Resolve a página de história — "quem somos desde quando" — quando a lista de datas é longa demais para caber empilhada e cada marco tem uma foto que vale mostrar. Uma linha do tempo vertical resolve a ordem, mas gasta uma tela inteira e transforma catorze anos em rolagem. Uma grade de cards resolve a foto, mas apaga a sequência: numa grade, 2012 e 2024 são vizinhos de tabela, não de tempo. O arco devolve as duas coisas ao mesmo tempo — a ordem está no gesto (o passado sai pela esquerda, o futuro entra pela direita) e a foto está em cada card. E, ao contrário de um carrossel comum, a roda tem começo e fim: o primeiro e o último card param no topo, sem laço, porque 2026 não vem antes de 2012.
Quando usar
- Página institucional de história, trajetória, "nossa jornada", marcos regulatórios de uma casa — 6 a 10 paradas, cada uma com data, um rótulo curto e uma foto.
- Quando a ordem importa e a repetição seria mentira. Se a fila pode dar a volta sem prejuízo, o componente é outro.
- Quando existe foto de verdade para cada parada. A roda é feita de imagem: sem foto, os cards viram retângulos coloridos e o arco não tem o que sustentar.
- Quando a dobra pode ocupar altura: com cabeçalho, palco e painel, a seção passa de 1100px em desktop e de 1100px em celular.
Quando não usar
- Menos de cinco paradas. Com quatro cards o arco ocupa 43° e a roda parece um leque torto; o encaixe também fica óbvio, porque as duas pontas estão sempre à vista. Use
listas/leque-paineis, que foi feito para 4 a 6 painéis. - Quando a fila é infinita por natureza — sócios, logos, depoimentos, cases. Ali o laço é a graça:
midia/carrossel-perfispara pessoas com bio,prova/carrossel-depoimentospara citações. - Quando cada item tem de levar a algum lugar. O card aqui não é link e não pode virar um sem quebrar o gesto (arrastar sobre um
<a>dispara navegação no fim). Usemidia/galeria-scroll-horizontal, em que o card é um<a>de verdade e o encaixe é nativo, ouoverlay/card-expansivel-fullscreenquando o clique abre o detalhe em tela cheia. - Quando o giro é o assunto e não a cronologia.
midia/carrossel-anel-3dé um cilindro: os cards ficam na parede de um tubo, com perspectiva e faces virando para dentro, e a fila fecha. A roda é plana, o giro acontece no plano da tela e a fila tem ponta. Escolha o anel para vitrine que dá volta; a roda para sequência que acaba. - Quando você quer um card grande no centro e os vizinhos só encostados, sem arco e sem inclinação:
midia/carrossel-foco-central. - Quando a leitura é de cima para baixo e cada etapa depende da anterior — método, processo, onboarding. Aí a rolagem é a metáfora certa:
listas/baralho-sticky. Umlistas/trilha-vertical-progressoestá previsto na fila para o caso puramente cronológico em vertical. - Quando as datas não têm foto e o que importa é o número:
prova/stats-contador-regua.
Padrão editorial
| Slot | Regra |
|---|---|
| Olho | Uma a três palavras. "Linha do tempo", "Trajetória", "Desde 2012". |
Título (h2.sec-title.ttl) |
4 a 7 palavras. Diz o arco inteiro, não a primeira parada: "Catorze anos, oito decisões". Evite "Nossa história", que não informa nada. |
Ano (.wheel-stamp e .wheel-year) |
O mesmo texto nos dois lugares. Quatro dígitos, ou "2012–14" se a parada cobrir um período. Acima de sete caracteres o carimbo do card encosta na borda. |
Rótulo do card (.wheel-label) |
Uma ou duas palavras, sempre substantivo. "Fundação", "Registro", "Porto Alegre". Ele é a etiqueta da parada, não o título — quem carrega a frase é o painel. Acima de 16 caracteres o rótulo quebra em duas linhas dentro do véu e o card fica com a base pesada. |
Título do painel (.wheel-title) |
22 a 48 caracteres, uma linha em desktop. É a frase do marco: "Registro como consultoria de valores mobiliários". |
Linha do painel (.wheel-text) |
80 a 100 caracteres, duas linhas em desktop. Diz o que mudou na prática, não o que a empresa sentiu. |
| Quantidade | 6 a 10 paradas, uma marca em .wheel-dots para cada. |
O que mantém a roda coesa é o paralelismo dos rótulos: se um é substantivo simples ("Registro"), todos são. Um "Abrimos o escritório de Porto Alegre" no meio de oito substantivos quebra a régua, porque os rótulos aparecem lado a lado no arco, três de uma vez, e a diferença de comprimento é lida como desalinhamento.
A segunda regra é de comprimento comparável no painel: a altura reservada (--wheel-panel-h) é fixa para os pontos não dançarem a cada troca, então uma parada de uma linha ao lado de outra de três deixa um buraco visível. Escreva as oito no mesmo tamanho.
Sobre a abertura assimétrica. A dobra nasce com o primeiro ano no topo e o lado esquerdo vazio — antes da primeira parada não há card. Isso é a leitura correta de uma linha do tempo, e a diagonal que desce à direita compõe com o título no alto à esquerda. Se a página precisar do quadro cheio dos dois lados na abertura, data-wheel-start="3" resolve, ao custo de a leitura não começar no começo.
Especificação dos assets
- Formato e dimensão mínima: retrato WebP, 900×1200 ou maior. O quadro exibido é 3:4 (288×384 em desktop, 226×302 em celular) e o
object-fit:covercorta pelas laterais. - Peso alvo: até 140 KB por foto. São 6 a 10 arquivos e só o primeiro carrega sem
lazy. - Composição / enquadramento: assunto no terço central-inferior. O terço de cima leva o carimbo do ano sobre véu escuro e a faixa de baixo leva o rótulo — rosto no topo do quadro sai atrás do número.
- Coerência de época é opcional, coerência de tratamento não é. As fotos podem ser de anos diferentes, mas têm de compartilhar temperatura e contraste: no arco aparecem três ao mesmo tempo, duas delas esmaecidas, e uma foto muito mais clara que as vizinhas parece um erro de opacidade.
- O que não pode ter: texto embutido na imagem (colide com o carimbo), logo de instituição real, moldura ou borda no arquivo (o canto vem de
--radius-media), captura de tela.
Como usar
<link rel="stylesheet" href="componentes/midia/roda-de-cards/estilo.css">
<script src="componentes/midia/roda-de-cards/script.js"></script>
O palco é irmão do .wrap, filho direto da <section> — é assim que ele sangra até a borda sem margem negativa e sem 100vw. Cada <li> traz o próprio --wheel-i no style e o conteúdo do painel nos data-wheel-year|title|text.
Ganchos
| Gancho | Papel |
|---|---|
[data-wheel] |
Raiz. O script parte daqui, e é nela que escreve --wheel-radius e --wheel-step medidos. |
[data-wheel-start] |
Índice da parada de abertura. Padrão 0. |
[data-wheel-list] |
O <ul> que cobre o palco: único ponto focável, alvo do arrasto e do teclado, dono de --wheel-angle. |
[data-wheel-prev] / [data-wheel-next] |
Os dois botões. Recebem aria-disabled nas pontas — nunca disabled. |
[data-wheel-panel] / [data-wheel-out="…"] |
A região aria-live e os três nós de texto reescritos na troca. |
[data-wheel-dots] |
A régua de marcas. |
--wheel-card / --wheel-card-h |
Largura do card e altura derivada (4/3). |
--wheel-h |
Altura do palco — quanto da descida do aro aparece. |
--wheel-rise |
Folga entre o topo do palco e o card do topo. 16px. |
--wheel-gap |
Vão entre cards no aro, como fração da largura do card. .1. |
--wheel-reach / --wheel-radius-min |
R = max(largura da seção × reach, min). .9 e 900 no desktop; 1.4 e 420 no celular. |
--wheel-radius / --wheel-step / --wheel-angle |
Escritos pelo script. Os valores no CSS são o desenho sem JavaScript. |
--wheel-drop |
Posição do cubo, derivada em CSS do raio, da altura do palco e da altura do card. |
--wheel-i / --wheel-k |
Índice do card (inline) e distância angular ao topo em passos (por quadro). |
--wheel-shrink |
Encolhimento por passo de distância. .05. |
--wheel-panel-h |
Altura reservada do painel. 186px. |
.is-dragging / .is-spinning / .is-swap / .is-on |
Cursor de arrasto · will-change ligado · animação de troca do painel · marca ativa. |
Variações
- abrir no meio —
data-wheel-start="3". - arco mais raso —
--wheel-reach:1.3: passo cai de ~14° para ~10° e os cards entram quase em pé. - mais paradas — mais
<li>com--wheel-iem sequência e mais uma marca em.wheel-dots. Acima de 12, troque a régua de pontos por contagem em texto. - sem painel — apague
.wheel-panel; o rótulo do card vira a única legenda e o padrão editorial dele muda de palavra para frase. Perde-se o retorno falado do teclado. - em seção clara —
.sec-paperno lugar de.sec-dark. O card não muda: ano e rótulo leem--lightsobre véu em--dark-rgb.
Gotchas
- O cubo da roda não é um elemento — é um ponto. Cada card é absoluto em
left:50%; top:calc(100% + var(--wheel-drop))e vai até o aro pelo própriotransform: translate(-50%,-50%) rotate(θ) translateY(-R). A ordem das funções é o componente inteiro: otranslatepõe o centro do card sobre o cubo antes do giro, e sem ele a rotação acontece em torno de um ponto que ainda não é o cubo. De brinde, o card sai inclinado no mesmo ângulo em que está no aro — a tangente vem de graça. Posicionar os cards no aro porleft/topexigiria seno e cosseno no JS e ainda pediria um segundotransformsó para inclinar. - O
<ul>cobre o palco e não gira; quem gira são os oito cards. Parece desperdício — um pivô único giraria com uma escrita só. O motivo é o foco: o<ul>é o único elemento focável da dobra, e um pivô que girasse levaria o anel de foco junto. No fim da linha do tempo o anel estaria a −99° e quase todo fora do recorte. Medido: com o quadro parado, o anel é um retângulo alinhado ao palco em qualquer índice. E o custo do "desperdício" é zero — ver o gotcha do desempenho. overflow:hiddenno palco come o anel de foco, e a solução éoutline-offset:-4px, nãopadding. O core desenha o anel 3px para fora da caixa. Dar respiro ao palco não resolveria: ele sangra até a borda da página, e o anel cairia sob ooverflow-x:hiddendo<body>— apareceria em cima e embaixo e sumiria nos lados. Offset negativo joga o anel para dentro do recorte. Mesma solução domidia/carrossel-perfis.- O passo angular é
2·asin((cardW + vão) / 2R), e não360/N. Dois cards vizinhos precisam ficar a (largura + vão) um do outro no aro, e essa distância é a corda de um ângulo. Com oito cards,360/Ndaria 45° por card — mais de três vezes o valor certo, que é 14,19° no desktop padrão (R = 1283px, card de 288px, vão de 10%). Com 45°, o vizinho do topo estaria a 907px do centro em vez de 313px, fora da tela, e o vão entre cards cresceria junto com o raio em vez de ficar constante. --wheel-gapé uma fração, não um comprimento, e isso é defesa contra oparseFloat. O script lê o vão degetComputedStyle().getPropertyValue(), que devolve o valor especificado de um custom property, não o resolvido: umclamp(14px,1.6vw,26px)chegaria como a string inteira eparseFloatpegaria só o14. Como fração da largura do card, o valor é sempre um número puro, escala junto com o card e dispensa override por breakpoint. Mesmo motivo para--wheel-reache--wheel-radius-minserem números soltos.- A altura do card é
calc(--wheel-card * 4 / 3)e nãoaspect-ratio:3/4. Parece pior e não é: a conta do cubo (--wheel-drop) precisa da altura em px dentro de umcalc(), easpect-rationão é legível ali. Trocar poraspect-ratiofaz o card do topo nascer fora do lugar, porque odroppassa a somar um termo vazio. will-changeentra e sai com o gesto. Deixado sempre ligado, cada card vira uma textura rasterizada uma vez e depois só transformada pelo compositor — e aí o ano e o rótulo ficam com a borda mole mesmo com a roda parada. Ligado só enquanto o laço roda (classe.is-spinningna lista), o texto é rasterizado na posição final assim que a roda encaixa, e o card do topo — que encaixa em rotação exatamente zero — sai nítido. Se alguém "consertar" movendo owill-changepara a regra base, o sintoma é texto permanentemente macio, e ninguém vai ligar uma coisa à outra.- Arredondar o ângulo não é remédio para tremor de subpixel. O script escreve
--wheel-anglecom três casas porque uma string de dezessete dígitos por quadro é desperdício, não porque a precisão importe: num raio de 1283px, 0,001° move o card por 0,022px. Quem resolve nitidez é owill-changeacima; quem resolve o pouso exato é opintar(exato)do gotcha seguinte. - O último quadro do encaixe escreve sem o corte de otimização. O
--wheel-kde cada card só é reescrito quando muda mais de 0,008, senão um quadro parado gastaria oito escritas para nada. O efeito colateral é que a roda pararia com o card do topo emopacity:0.99e o vizinho meio grau fora do lugar — invisível de uma vez, acumulado depois de vinte trocas. No quadro em que a roda assenta, a escrita é forçada. Medido: com o forçamento,--wheel-kfica em1.00 / 0.00 / 1.00e as opacidades em0.45 / 1 / 0.45exatos. - Escrever
--wheel-knos oito cards por quadro é de graça, e o número surpreende. Medido em 1440×900, por quadro simulado com recálculo forçado: reflow sozinho 0,001ms; mais a escrita de--wheel-anglena lista 0,88ms; mais as oito escritas de--wheel-k0,71ms — custo marginal negativo dentro do ruído. A razão é que a escrita do ângulo já invalida os mesmos oito cards: o recálculo aconteceria de qualquer jeito. As nove chamadas desetPropertyem si custam 0,01ms. E o corte de escrita faz com que, num arrasto rápido (0,9°/quadro), apenas 2,86 dos 8 cards recebam escrita por quadro, porque quem já está no teto dekparou de mudar. - A borracha das pontas começa quase 1:1 e satura. A conta é
teto·e/(e+teto)comteto = 2 passos. Medido no índice 0, arrastando 1280px para a direita: sem borracha o giro seria de 57,16° (quatro passos); com ela, 18,97° (1,34 passos), e os primeiros 80px rendem 3,17° contra 3,57° livres — 89%, ou seja, o começo do gesto parece normal e o travamento chega depois. Ao soltar, o encaixe devolve a 0,000° exatos. Não confunda com a inércia: um arremesso que chega na ponta para seco, sem quicar, porque quique em ponta lê como defeito e não como limite. - Inércia e encaixe são por quadro, não por tempo — e aqui isso é correto. O contrato pede movimento contínuo por
dt, mas nada nesta dobra anda sozinho: os dois só existem depois de um gesto e duram menos de meio segundo. Medido: o encaixe de um passo leva 45 quadros para chegar a 0,005° e cerca de 25 quadros (420ms a 60Hz) para ficar a menos de 7px de arco do destino. Numa tela de 120Hz isso vira ~210ms — diferença que ninguém percebe num gesto que a pessoa acabou de fazer. É a mesma convenção domidia/carrossel-perfis; o que lá é por tempo é a esteira que anda sozinha, e aqui não há nenhuma. - Os cards não são focáveis, de propósito. Oito cards focáveis dariam oito paradas de
Tabdentro de uma caixa que corta e gira, e o navegador tentaria rolar o recorte atrás de cada um — o mesmo problema descrollLeftfantasma que omidia/carrossel-perfisteve de absorver, agravado por o contêiner estar girado. Em vez disso há uma parada só, a lista, com ←, →,HomeeEnd; clicar num card lateral vai até ele e quem usa teclado chega ao mesmo lugar pelas setas. O conteúdo de todas as paradas está no DOM em qualquer estado. - Os botões usam
aria-disabled, nãodisabled. Um botão que ganha o atributodisabledno exato momento em que está com o foco sai da ordem de tabulação, e o foco cai no<body>: quem chegou de teclado à ponta da linha do tempo é jogado para fora da dobra. Comaria-disabledele apaga, continua focável e o script ignora o clique. É uma correção sobre omidia/galeria-scroll-horizontal, que ainda usadisabled. - O teclado conta a partir de
destino, não deativo. Duas setas em quadros seguidos têm de avançar duas paradas.ativosó muda quando o painel troca; basear a conta nele engoliria a segunda tecla, e o defeito só aparece com quem digita rápido. - A troca do painel força um reflow, e ele é obrigatório.
classList.remove('is-swap')seguido deaddno mesmo quadro é agrupado pelo navegador, que conclui que a classe nunca saiu — a animação não reinicia. A leitura deoffsetWidthentre os dois é o que quebra o agrupamento. É uma vez por troca de índice, nunca por quadro. - A região
aria-livesó é reescrita na troca de índice, edata-wheel-starté aplicado sem animação. Reescrever por quadro faria o leitor de tela falar sem parar durante o arrasto; aplicar a parada de abertura com a animação de troca faria a região mudar no meio da leitura inicial da página.aria-atomic="true"existe porque "2014" anunciado sozinho não diz nada — com ele, ano, título e linha saem como uma frase só. - O véu de cima não tem
backdrop-filtere o de baixo tem. São oito cards girando ao mesmo tempo, e cadabackdrop-filterobriga o compositor a reler o que está atrás a cada quadro. O de baixo paga esse preço (blur(3px), o mesmo domidia/galeria-scroll-horizontal) porque é onde mora texto corrido sobre foto — e vai sempre com máscara em gradiente, senão a borda reta do desfoque aparece (contrato §6). O de cima carrega quatro dígitos em corpo grande e contraste alto, e gradiente puro basta. Não medido: o custo de paint dessebackdrop-filterem hardware real — o headless não tem vsync e não serve para essa medida. É o primeiro número a conferir no primeiro site que usar. - O palco é mais baixo no celular por conteúdo, não por espaço de tela. Com o card em 226px e o passo em 26,3°, os vizinhos param de descer aos 373px; os 440px proporcionais ao desktop deixariam 67px de palco vazio embaixo, que o olho lê como buraco entre a roda e o painel. Daí
min(54vh,392px). --wheel-panel-hé altura reservada, não altura mínima de estilo. Sem ela os pontos sobem e descem a cada troca de parada, e isso lê como defeito. Medido: a parada mais alta dá 176px em 1440 e 177px em 390; o valor é 186px para cobrir as larguras entre os dois. Aumentar o limite de caracteres do painel pede aumentar isto junto.
Histórico
| Projeto | Onde | Data | Diferenças |
|---|---|---|---|
| autoral | construído do zero na biblioteca | 2026-09-03 | — |
Origem. Padrão de interação, não clone de site: a roda-gigante de cards com o cubo abaixo da dobra aparece em muitos lugares — vitrines premiadas do Awwwards, componentes de carrossel em arco distribuídos para Framer, e as páginas de "nossa história" de marcas de moda e de automóveis, que quase sempre põem os anos num arco em vez de numa lista. Nenhuma delas foi a referência: não há URL, não há captura, e não há uma linha copiada. A forma foi construída aqui a partir da geometria — corda, raio, tangente —, e o resto saiu do contrato. Antes de construir, o catalogo.json foi consultado: nenhum componente catalogado resolvia sequência com ponta e foto por parada. Status externo: nunca rodou em produção. O primeiro site que usar promove, e esta ficha registra qual foi. O registro consolidado está em projetos/autorais.md.
Decisões de tradução para o contrato.
- A geometria virou CSS, não JavaScript. O cubo é um ponto, cada card vai ao aro pelo próprio
transform, e o arco continua desenhado semscript.js— nos valores de fallback (--wheel-radius:1100px,--wheel-step:14deg), os cards não empilham num ponto só. O que o script traz é a medida: raio a partir da largura da seção, passo a partir da largura do card. - Toda medida fixa virou variável do prefixo: largura e altura do card, altura do palco, folga do topo, vão no aro, alcance e piso do raio, encolhimento por passo, altura reservada do painel. Nenhuma cor crua: véus em
rgba(var(--dark-rgb),…), fio do card ativo emcolor-mixsobre--accent, texto sobre foto em--light, o resto herdado do escopo de tom. - A sangria segue a regra da casa: o palco é irmão do
.wrap, filho direto da seção. Nenhuma margem negativa, nenhum100vw(§4). - A entrada é do core:
data-rvno palco e no rodapé,data-dpara escalonar,.ttlno título. Nenhum observer próprio para "aparecer". O únicoIntersectionObserverdo componente serve para dormir, não para revelar. - O painel entra com blur indo a zero (§5), por
animationreiniciável e não portransition, para poder disparar de novo no mesmo elemento sem depender do estado anterior — mesma razão do.ttl. - Acessibilidade que o padrão comum não tem: um único ponto focável com setas,
HomeeEnd, em vez de oito paradas deTabdentro de uma caixa que corta e gira; anel de foco retangular, porque quem recebe foco não gira;aria-roledescription="carrossel"e rótulo na lista;aria-currentno card do topo; painelaria-live="polite"comaria-atomic, reescrito só na troca; marcas do indicador emaria-hiddenpor serem gráficas; botões comaria-disabledpara o foco não cair no<body>na ponta;touch-action:pan-ypara a página continuar rolando no toque vertical;dragstartcancelado para a foto não virar drag-and-drop de arquivo. - Movimento reduzido: nada anda sozinho em estado nenhum. Com
reduce, o arrasto continua funcionando (gesto pedido não é movimento imposto), mas sem inércia e sem aproximação — medido: o encaixe leva 1 quadro em vez de 45 —, e a animação do painel fica emnone.
Divergências conhecidas vs. o desenho pedido.
- Não existe um elemento
.wheel-hub. O desenho original previa um pivô que girasse, com os cards no centro dele. Ele foi trocado pelo ponto + transform por card, pelo motivo do segundo gotcha (foco), e o custo medido é zero. A conta do cubo e a ordem das funções de transform são as mesmas. - Os botões ficam sob o título no celular, e não abaixo do painel. Mover um controle de container entre larguras exigiria dois conjuntos no DOM — dois pares de controles anunciados pelo leitor de tela — ou um grid que quebraria a conta do
.wrap. Em tela de toque o gesto principal é o arrasto, e os botões são a alternativa, não o caminho. -
A régua de marcas não é clicável. Oito botões a mais na ordem de tabulação, ao lado de uma navegação que já existe em três formas, atrapalhariam mais do que ajudam. Se um projeto precisar de acesso direto a uma parada distante, o caminho é transformar cada
.wheel-dotem<button>comaria-labeldo ano — e aí a régua sai doaria-hidden. -
Os dois botões são a primitiva
.btn-discdo core, e o markup traz as duas classes (class="btn-disc wheel-btn"). A regra do disco estava copiada ao caractere em cinco pastas —midia/galeria-scroll-horizontal,midia/carrossel-anel-3d,midia/carrossel-foco-central,midia/pilha-swipeemidia/roda-de-cards— e foi promovida aocore/base.cssem 2026-09-03, como o §6 do contrato manda fazer já na terceira. Forma, cor, hover e o estado desabilitado vêm de lá; o parafuso é--disc-size. Não é.btn: aquela é pílula de texto, com padding horizontal fixo. A primitiva atende as duas formas de desabilitar,:disablede[aria-disabled="true"]— e em carrossel a segunda é a recomendada, porquedisabledtira o botão da ordem de tabulação no exato momento em que ele está com o foco, e quem chegou de teclado à última seta é jogado para o<body>. Esta dobra foi a primeira a precisar dessa variante, e ela foi junto na promoção.