Uma primitiva de efeito, não uma seção: qualquer elemento que receba o atributo [data-tilt] passa a se inclinar em 3D na direção do ponteiro — até um teto configurável de graus — com um brilho (glare) que desliza pela superfície acompanhando a mão, e volta ao plano com uma mola suave quando o ponteiro sai. É o que faz um card de vidro parecer um objeto que reage à presença de alguém, em vez de uma imagem parada.
Resolve um problema de acabamento, não de conteúdo: uma grade de cards já pronta (soluções, equipe, planos) muitas vezes está correta na informação e ainda assim parece "chapada" — sem peso, sem a sensação de superfície. Uma lista comum resolveria a informação; o que falta não é organização, é presença. A inclinação 3D é o "uau" barato que se aplica por cima de qualquer grade da biblioteca sem mexer no componente de baixo: cola o atributo, os dois arquivos, e pronto. Nenhum componente de card precisa ser reescrito para ganhar o efeito, e nenhum precisa perdê-lo para voltar a ser só .glass — basta tirar o atributo.
Quando usar
- Grade de 2 a 6 cards de vidro (
.glass) ou superfície equivalente, numa dobra onde o mouse é o gesto principal — hero, seção "como funciona", grade de soluções. - Seção de tom escuro (
.sec-dark,.sec-deep). O glare lê--lighta 18%: sobre um card já claro (.glassnuma.sec-light), o brilho tem pouco contraste para aparecer — a mesma lógica por trás de.glassjá nascer pensado para fundo escuro. - O card já carrega conteúdo suficiente por si (número, título, texto curto): a inclinação é acabamento, não é o que transporta a informação.
- Projeto que já usa
.glassem algum lugar e quer que essa grade específica pareça mais tocável, sem tocar no CSS do card em si. - Poucos cards por dobra — até seis, na prática. Cada
[data-tilt]reserva sua própria camada de composição (ver Gotchas sobrewill-change); uma grade de vinte cards pedindo o efeito ao mesmo tempo é candidata a serrilhado e a gasto de memória de vídeo sem benefício visual proporcional.
Quando não usar
- Público majoritariamente em toque: o efeito não existe abaixo do guard (ver §"Movimento reduzido / toque"), e o card fica exatamente como
.glasssozinho. Se o design depende do tilt para "funcionar" e o público é majoritariamente celular, prefira algo que reaja também a toque — o.glass-hoverdo próprio core (borda e brilho no:hover, sem 3D) é a versão sóbria que não desaparece em lugar nenhum. - Quer destacar um argumento específico dentro de uma dobra, não dar peso a uma superfície inteira: isso é outro efeito, com outra mecânica —
efeitos/cursor-anotacao, que aponta para um texto com uma bolha, e não inclina nada. - O card já roda a própria animação pesada por dentro (rodízio, gráfico recalculando): duas animações competindo no mesmo card é ruído, não reforço. Ver
conteudo/trio-cards-ui-simulada, cujos cards já têm interação própria. - O elemento vai conter algo com
position:fixeddentro (um selo, um botão que devia flutuar sobre a página inteira):transformcria um novo containing block, e ofixedpassa a ser relativo ao card, não à viewport. Ver Gotchas.
Padrão editorial
Esta tabela descreve o palco desta ficha (a dobra "como funciona" que demonstra o efeito) — não o card de quem for só copiar [data-tilt] para uma grade já existente, que segue o padrão editorial do componente de origem daquele card.
| Slot | Regra |
|---|---|
| Numeral | Dois dígitos, aria-hidden (decorativo — a ordem real vem da posição no DOM). |
| Título | Uma palavra ou substantivo curto. Nomeia a etapa, não a explica. |
| Texto | Uma frase, 70 a 130 caracteres. É o card inteiro que precisa continuar legível em repouso, sem o efeito. |
| Pílula | Etapa N. Reaproveita .pill do core, sem variação de cor. |
O que mantém a grade coesa é o paralelismo entre os três textos: mesmo comprimento aproximado, mesma estrutura de frase (sujeito oculto + o que acontece). Card com texto muito mais longo que os vizinhos cresce em altura e desalinha a fileira, já que os três dividem a mesma grade.
Especificação dos assets
Nenhum. [data-tilt] não usa imagem, ícone nem fonte própria — atua inteiramente sobre o que já existir dentro do elemento marcado, com CSS puro (radial-gradient + color-mix) para o brilho. Convive sem conflito com .glass-hover do core no mesmo elemento: um muda border-color e background no :hover, o outro muda transform e a opacidade do ::after — propriedades diferentes, sem disputa de cascata.
Como usar
<link rel="stylesheet" href="componentes/efeitos/inclinacao-3d/estilo.css">
<script src="componentes/efeitos/inclinacao-3d/script.js"></script>
O uso real não é copiar a dobra de demonstração — é marcar um card que já existe em outro componente:
<!-- um card de qualquer outra grade da biblioteca -->
<div class="glass minha-grade-card" data-tilt>
...
</div>
Nada mais muda no componente de baixo: nem a folha de estilo dele, nem o script dele, se tiver um. Confira sempre onde o data-rv mora antes de colar o atributo. Em conteudo/grid-entidades, por exemplo, o data-rv do card está no mesmo <article class="glass glass-hover ent-card" data-rv> que receberia o data-tilt — colar direto ali reproduziria o primeiro Gotcha desta ficha. O card certo para acompanhar precisa primeiro separar as duas coisas (o data-rv sobe para um wrapper, como este componente faz com .tilt-card-slot), e só depois recebe data-tilt livre por dentro.
Ganchos
| Gancho | Papel |
|---|---|
[data-tilt] |
A raiz do efeito. Qualquer elemento com o atributo passa a inclinar; o script varre o document inteiro atrás dele (primitiva global — ver Gotchas). |
[data-tilt-demo] |
Marca só a seção de demonstração desta ficha. O script não lê este atributo. |
--tilt-max |
Graus no pico do gesto, lido por card via getComputedStyle. Padrão 7, sem unidade. |
--tilt-lift |
Escala no pico da inclinação, só enquanto .is-tilting. Padrão 1.02; 1 desliga o "levantar". |
--tilt-persp |
Distância da perspectiva, dentro do próprio transform (sem pai controlado — ver Gotchas). Padrão 900px. |
--tilt-x / --tilt-y |
Ângulos que o script escreve a cada quadro em movimento. Estado interno; não se ajusta à mão. |
--tilt-gx / --tilt-gy |
Posição do brilho, em % da caixa do card. Escrito junto com tilt-x/tilt-y. |
--tilt-scale |
Ponte interna entre .is-tilting e --tilt-lift, para o transform ter uma única lista de funções (ver Gotchas). Não é API pública. |
.is-tilting |
Presente do pointerenter em diante, mas só a partir da segunda escrita do quadro. Desliga a transição e acende o brilho. |
Variações
--tilt-maxpor card —style="--tilt-max:4"no elemento marcado deixa aquele card mais discreto que os vizinhos. Tem de estar no HTML desde o carregamento (ver Gotchas: o valor é lido uma vez só).- Sem o brilho — apague
[data-tilt]::aftere[data-tilt].is-tilting::after. Sobra só a inclinação, para card que já tem textura própria. - Sem o levantar —
--tilt-lift:1. O card inclina sem crescer 2% no pico: mais sóbrio, e evita por completo o serrilhado de borda medido no gotcha do.glass. - Inclinação mais forte —
--tilt-max:10a12no card de destaque de uma grade (o card de plano recomendado, por exemplo). Acima disso a perspectiva de900pxcomeça a distorcer visivelmente; suba--tilt-perspjunto se precisar de mais. - Conteúdo carregado depois — limitação, não configuração: o script varre
[data-tilt]uma vez, na carga da página. Ver Gotchas. - Um card só, em destaque — nada impede usar
[data-tilt]num único elemento fora de uma grade (um card de preço em evidência, um convite isolado). O efeito não sabe que existe uma "grade"; o palco desta ficha é só o caso mais comum.
Gotchas
data-rvnunca no mesmo elemento quedata-tilt— descoberto medindo, não por instinto.[data-rv].in(a classe que o core aplica quando a entrada termina) declaratransform:none, e tem especificidade (0,2,0) — um atributo mais uma classe.[data-tilt]é só um atributo: especificidade (0,1,0). Numa grade cujos cards usamdata-rvpara entrar na dobra, se o mesmo atributodata-tiltmorasse no mesmo nó,[data-rv].invenceria a cascata para sempre depois da revelação — não por ordem de arquivo (estilo.csscarrega depois decore/motion.css), mas por especificidade, que ignora ordem.
O efeito continuaria "ligado" por dentro — JS escrevendo --tilt-x, classe .is-tilting alternando — e completamente invisível por fora, porque a regra que lê essas variáveis simplesmente perde a guerra antes do script entrar em cena. Medido: com os dois atributos no mesmo <div>, getComputedStyle(card).transform fica em "none" mesmo com --tilt-x e --tilt-y diferentes de zero — um card clonado à mão, fora da página, com as mesmas classes e os mesmos atributos, mostra a matriz correta, o que descarta hipótese de erro de seletor e aponta direto para a especificidade do .in herdado do card real. A correção é a mesma separação que midia/carrossel-perfis e midia/galeria-scroll-horizontal já fazem entre o item da lista e o card interativo: aqui, um .tilt-card-slot sem estilo próprio carrega o data-rv, e o [data-tilt] fica livre por dentro, um nível abaixo.
- A primeira escrita entra com a transição ligada, de propósito — sem isso o card "pula". Se
.is-tilting(que desativa atransition) fosse adicionada já nopointerenter, a primeiríssima escrita do quadro — que pode ser um ângulo perto do teto, se o ponteiro entrar por um canto — apareceria instantânea, sem nenhuma suavização: um salto seco de 0° para 6-7°. O script adia.is-tiltingpara a segunda escrita; a primeira usa atransition:transform .55s var(--ease)de repouso.
Medido entrando por um canto (max 7°, alvo 6.30°/6.30°): nos quadros 1 e 2 o transform ainda é o de repouso (0% do caminho — o motor de transição leva um quadro para "perceber" a mudança); no quadro 3, 4,1%; quadro 5, 30,4%; quadro 8, 40%; quadro 13, 53,6%; quadro 20, 67,6%; quadro 33 (~550ms, a duração declarada), 83,1%; quadro 45, 90,9% — a curva cubic-bezier(.16,.84,.24,1) tem uma cauda longa, então "83% na duração nominal" é esperado, não bug. A partir da segunda escrita, com .is-tilting já presente, o rastreio é instantâneo: medido movendo o ponteiro para o canto oposto, o transform do quadro seguinte já é idêntico ao dos quadros 2 e 5 depois dele — nenhuma interpolação, puro acompanhamento quadro a quadro.
backdrop-filterdo.glass+rotate3dserrilha o canto arredondado, mas só onde existe canto. Medido com screenshot ampliado (crop 8×,Image.NEAREST): no tema padrão desta biblioteca (--radius-media:0, cantos vivos), a borda do card inclinado sai limpa — não há curva para serrilhar. No tema Kirvano (--radius-media:16px), o mesmo card, na mesma inclinação e escala (1,02), mostra um degrau visível na curva do canto.
A comparação que isola a causa: o canto do mesmo card em repouso, no mesmo recorte e no mesmo zoom, sai perfeitamente antialiased — então não é o border-radius em si, nem o zoom da captura. A diferença é só a inclinação. A explicação mais provável é a combinação de backdrop-filter (que força uma camada de composição própria, separada do resto da página) com um transform 3D aplicado a essa mesma camada: o recorte arredondado dela não é re-antialiased com a mesma qualidade quando a camada inteira gira. Não há correção de CSS para isto — é o preço de vidro fosco girando, e em tema com --radius-media grande vale saber que ele existe antes de aprovar a dobra. A variação "sem o levantar" (--tilt-lift:1) reduz um pouco o efeito (menos escala, menos recorte se movendo), mas não elimina.
-
--tilt-maxé lido uma única vez, na inicialização do card — não nopointerenter.iniciar(card)roda para cada[data-tilt]na carga da página e guardamaxnuma variável de closure. Medido: escrevercard.style.setProperty('--tilt-max','4')via console depois da carga não muda nada no próximo hover — o card continua usando o valor lido no load. Jástyle="--tilt-max:4"no HTML desde o início funciona exatamente como esperado (medido: 3,60° em vez de 6,30° no mesmo canto). A variação "por card" é real, mas só como marcação estática — não como controle dinâmico. -
900pxde perspectiva é calibrado para o tamanho de card desta faixa, não um número neutro. Perspectiva baixa (300-400px) exagera a distorção: nos cantos do card, o "puxão" fica visivelmente curvo, tipo olho de peixe. Perspectiva alta (2500px+) achata o efeito até ficar imperceptível no teto de 7° do--tilt-max.900pxfoi o valor que, medido nos cards de ~250-350px de largura do palco desta ficha, ainda lê como "objeto sólido girando" sem ler como "lente". Card muito maior ou muito menor que essa faixa pode pedir ajustar--tilt-perspjunto — os dois parafusos (graus e perspectiva) andam juntos, não separados. -
perspective()mora dentro do própriotransform, não como propriedade do pai — e isso é uma escolha, não descuido. O jeito "de manual" de fazer um grupo de cards inclinar com um vanishing point comum éperspectivena propriedade do contêiner, com os filhos girando dentro dela.[data-tilt]é uma primitiva que precisa colar em qualquer elemento, sem exigir que quem usa prepare o pai — não há pai controlado. A consequência prática: cada card tem seu próprio ponto de fuga, centrado nele mesmo. Numa grade, isso lê bem porque cada card parece um objeto independente reagindo à mão; não leria bem se a intenção fosse simular uma prateleira única vista em perspectiva — para isso, este componente é a peça errada. -
transformno card cria um novo containing block — mesmo em repouso.perspective(900px) rotateX(0deg) rotateY(0deg)não é a palavra-chavenone: é umtransformválido que, mesmo sem rotação nenhuma visível, já é o suficiente para o spec de CSS tratar o elemento como containing block dos seus descendentesposition:fixed.
Um selo ou botão position:fixed dentro de um [data-tilt] vira relativo ao card, não à viewport — e cola errado assim que a página rola, porque o elemento "fixo" passa a rolar junto com o card em vez de ficar parado na tela. Fora do guard de hover (mobile, reduzir movimento), a regra inteira nem existe, então esta pegadinha é só em desktop com hover fino — mas é justamente ali que alguém prepararia a página olhando para a tela e aprovaria o layout sem notar o problema, porque um selo fixed dentro de um contêiner sem scroll próprio se comporta como esperado até a página rolar de verdade.
-
--tilt-scaleexiste para otransformter uma lista de funções só..is-tiltingnão reescreveperspective()/rotateX()/rotateY()— ela só muda o valor de--tilt-scalede1(padrão) paravar(--tilt-lift). As duas regras ([data-tilt]e[data-tilt].is-tilting) então compartilham a mesma lista de funções na mesma ordem, e isso importa: o navegador interpolatransformfunção a função quando as duas pontas de uma transição declaram a mesma lista; listas diferentes forçam interpolação de matriz cheia, que funciona mas satura o ganho de 2% do "levantar" com o resto do gesto. Reescrever a lista inteira a cada troca de estado teria o mesmo efeito visual e seria pior de manter. -
[data-tilt]não ganhaoverflow:hidden, de propósito. Cortar o transbordo cortaria também o anel de:focus-visiblede um card que, em algum uso futuro, seja também um link ou um botão — o core desenha esse anel 3px para fora da caixa.border-radius:inheritno::afterdo brilho resolve o canto arredondado sozinho:background(mesmo em gradiente) já respeita o raio da própria caixa sem precisar de nenhum corte por cima. -
pointerenter/pointermove/pointerleave, nãomouseenter/mousemove/mouseleave— por consistência, não por necessidade. O guard já garante ponteiro fino com hover (exclui toque), então as duas famílias de evento fariam o mesmo aqui. A escolha importa por um detalhe que as duas famílias compartilham e que a alternativa errada (pointerout/mouseout) não tem:pointerleave, comomouseleave, não borbulha pelos filhos. Mover o ponteiro do padding do card para dentro do título ou da pílula não dispara um "saiu e entrou de novo" a cada cruzamento de borda interna. -
Não existe versão por teclado, de propósito — não por esquecimento. O gesto é geometricamente uma posição (x, y) dentro da caixa do card; foco por Tab não carrega posição nenhuma, só "entrou" ou "saiu". Simular uma posição arbitrária (por exemplo, sempre o centro) daria
--tilt-x/--tilt-yzerados — nenhuma inclinação, só o brilho acendendo no meio do card sem razão aparente para quem navega por teclado. Como o card já é.glasslegível sem o efeito (mesma garantia do toque e do movimento reduzido), a ausência de uma versão por foco não tira informação nem interação de ninguém — só não reproduz um gesto que o teclado não tem como descrever. -
A busca parte do
document, não de uma raiz local — a única exceção da biblioteca a este respeito, e é deliberada. Todo outro componente aqui varre a própria raiz ([data-<pref>]) para não colidir se a página tiver duas instâncias.[data-tilt]é uma primitiva global, do mesmo jeito que[data-rv]e[data-split]são globais emcore/motion.js: ela precisa alcançar qualquer card, em qualquer dobra, sem que este arquivo saiba de antemão onde. Não há raiz para partir — a ausência dela é o ponto. -
will-change:transformestá dentro do mesmo@mediado efeito, e isso é o que faz o custo desaparecer em toque.will-changepede ao navegador uma camada de composição própria — memória de vídeo reservada, não CPU — para cada elemento que casa com a regra, hover ativo ou não. Como a declaração mora dentro de@media(hover:hover) and (pointer:fine), em qualquer tela de toque ela simplesmente não existe: zero camadas extras, zero custo. Em desktop, ela existe para todo[data-tilt]da página o tempo todo — daí o teto prático de "até seis por dobra" em Quando usar: não é limite de beleza, é limite de quantas camadas vale reservar para um efeito que só se paga quando alguém passa o mouse. -
Conteúdo injetado depois da carga não ganha o efeito, e não há como "religar". O script roda uma vez, no carregamento da página. Um card que chega depois — aba trocada, paginação, resultado de busca — nasce sem
[data-tilt]funcional, porque a IIFE já rodou e não fica escutando por elementos novos (ao contrário deLIB.scan(), que o core expõe exatamente para este cenário nos reveals). Não expusemos umLIB.tilt()equivalente de propósito — mantém o arquivo com zero pegada global, como pede o contrato. Quem precisa do efeito em conteúdo dinâmico deve garantir que[data-tilt]já esteja no HTML entregue pelo servidor. -
Este efeito não obedece a
?motion=on. O parâmetro força as animações docore/motion.jsmesmo com "reduzir movimento" ligado no sistema, para gravar vídeo em máquina alheia — mas só ajuda quem checaLIB.reduceem JavaScript. A regra que desliga otransformaqui é@media(prefers-reduced-motion:reduce)pura, sem essa checagem, exatamente como.pfl-strip-rail,.depo-tracke.cur-notejá fazem: nenhuma transição pura-CSS da biblioteca reage ao parâmetro. E mesmo que reagisse, não haveria o que mostrar: o efeito depende de um ponteiro de verdade se movendo, não de uma animação que se repete sozinha.
Movimento reduzido / toque
Nada acontece — o card fica plano. Não existe fallback visual porque não precisa: o card já é .glass (ou equivalente) por baixo, e isso já é bonito parado. Medido: em --mobile (390×844, toque) e em --reduce (prefers-reduced-motion:reduce), os três cards do palco mostram getComputedStyle(card).transform === "none" e nunca ganham .is-tilting, mesmo disparando um pointerenter sintético do tipo touch sobre eles — porque o script, no guard de topo, nunca chega a anexar um único listener. Em --mobile especificamente, --tilt-max sai vazio de getComputedStyle: a regra inteira, incluindo o valor-padrão da variável, está atrás da mesma media query do script, então nem a declaração existe fora de ponteiro fino com hover.
Isso é a definição de "uau" que o contrato pede (§4): não é quantidade de efeito, é uma ideia clara que desiste completamente quando as condições não a sustentam, em vez de degradar mal. Um "tilt" que tentasse simular alguma coisa em toque — seguir o giroscópio do aparelho, por exemplo — seria outro componente, com outro orçamento de risco (giroscópio pede permissão em iOS, drena bateria, e o gesto de "inclinar o celular para ver o brilho" não é descoberto por ninguém sem instrução na tela).
Histórico
| Projeto | Onde | Data | Diferenças |
|---|---|---|---|
| autoral | construído do zero na biblioteca | 2026-09-03 | — |
Origem. Padrão de card que segue o ponteiro em 3D ("tilt card"), recorrente em sites premium e de produto: aparece em páginas vendidas como template no Awwwards, no marketplace de componentes da Framer, e a biblioteca open-source vanilla-tilt.js populariza a mesma matemática (rotação a partir da posição relativa do ponteiro dentro do elemento) desde 2017. Nenhuma dessas foi consultada linha a linha — o que se conhecia era o resultado visual do padrão, não o código de nenhuma implementação específica. Nada foi copiado: reescrito do zero dentro do contrato desta biblioteca.
Decisões de tradução para o contrato. Os ajustes de comportamento — teto de graus, escala no pico, distância de perspectiva — viraram variáveis do próprio atributo (--tilt-max, --tilt-lift, --tilt-persp) em vez de opções de configuração passadas para uma função de inicialização, como a maioria das implementações de mercado faz; isso segue a mesma regra dos demais componentes da casa (ajuste público é variável CSS na raiz, não parâmetro de JS). O brilho é CSS puro — radial-gradient lendo --light do escopo de tom, mais color-mix — em vez de canvas ou uma imagem de brilho, então funciona em qualquer tema sem asset.
A entrada ganhou uma suavização que o padrão comum não tem (a primeira escrita fica com a transição de repouso ligada, só a partir da segunda é que o rastreio vira instantâneo — ver Gotchas), e a ativação ganhou dupla trava de acessibilidade: CSS e JS checam a mesma condição de mídia (hover:hover e pointer:fine) para nunca acordar em toque, e o JS ainda confere movimento reduzido antes de anexar um único listener — a maior parte das implementações de mercado só checa 'ontouchstart' in window, sem checar prefers-reduced-motion nenhuma vez. Também não veio do padrão comum a separação entre data-rv e data-tilt em elementos diferentes: foi uma correção descoberta construindo este componente (ver primeiro Gotcha), não uma prevenção — o palco original desta própria ficha tinha os dois atributos juntos até a medição expor o conflito de especificidade.