Curseurs✨ Premium

Cursor Tilt Effect

Le pointeur pilote rotateX/rotateY jusqu'à ±15° et un reflet radial via deux variables CSS, retour en 500 ms à la sortie. La chaîne transform écrite par le JS commence par perspective(600px) : la carte bascule vraiment, bord proche qui grandit et bord lointain qui rétrécit, sans rien exiger du parent.

JSCSS 3DPerspective

Vous aurez accès à la démo interactive avec un compte gratuit.

Cet effet fait partie d'Effect.Labs — 811 effets vanilla, une partie gratuite, une partie premium. Curseurs comprend 26 effets, dont 1 gratuits. Découvrir la catégorie →

3 exemples d'usage

Carte de tarif — plan mis en avant — Cursor Tilt Effect exemple 1

① Carte de tarif — plan mis en avant

QuandGrille de tarifs où le plan recommandé est isolé dans une carte plus grande, avec prix, liste d'inclus et bouton.
PourquoiL'inclinaison et l'ombre indigo qui apparaît au survol donnent du relief au plan qu'on veut vendre, sans toucher au texte. Sur une carte de 330 px, ±15° reste modéré ; le reflet qui suit la main rend la carte « matérielle ».
RéglagesCarte 330 × 236 px, perspective(600px) conservée dans le JS, amplitude ramenée à 10 (les deux coefficients), scale(1.03) pour ne pas chevaucher les cartes voisines.
Vignette de projet — grille de portfolio — Cursor Tilt Effect exemple 2

② Vignette de projet — grille de portfolio

QuandMosaïque de projets : visuel plein cadre, titre et catégorie en bas de vignette.
PourquoiSur une image, le reflet de 12 % joue le rôle d'un vernis qui accroche la lumière, et l'inclinaison distingue la vignette survolée sans zoom brutal. Le retour en 500 ms avec la courbe cubic-bezier(.23, 1, .32, 1) repose la carte doucement.
RéglagesVignette 360 × 226 px, perspective(800px) dans le JS pour une bascule plus douce, reflet renforcé à rgba(255,255,255,.18), ombre 0 24px 48px rgba(0,0,0,.45) neutre plutôt qu'indigo.
Badge d'événement — pass nominatif — Cursor Tilt Effect exemple 3

③ Badge d'événement — pass nominatif

QuandCarte de participant : nom, rôle, code QR, date et lieu, présentée comme un objet à manipuler.
PourquoiUn badge est un objet physique : l'inclinaison qui suit la main et le reflet reproduisent la carte plastifiée que l'on tourne sous la lampe. La rotation reste sous ±15°, le code QR reste lisible à tout instant.
RéglagesCarte 340 × 210 px, perspective(500px) dans le JS pour accentuer le relief, amplitude 15 conservée, overflow: hidden gardé pour le reflet — le translateZ du contenu restera sans effet, c'est attendu.

Comment ça marche

Chaque [data-tilt-card] (200 × 140 px, dégradé 145deg #1e1e3a → #2a2a4e, border-radius: 16px) écoute mousemove. Le script mesure la carte (getBoundingClientRect), calcule la position du pointeur par rapport au centre et en tire deux angles : rotateX = (y − cy) / cy × −15 et rotateY = (x − cx) / cx × 15, soit ±15° aux bords (mesuré : 13,7° à 97 % de la largeur), 0° au centre. Il écrit transform: perspective(600px) rotateX(…) rotateY(…) scale(1.05) et box-shadow: 0 20px 40px rgba(99,102,241,.3) en ligne. Les signes sont choisis pour que le bord sous le pointeur recule, comme si on appuyait dessus.

Le reflet est un calque .tilt-shine en position: absolute; inset: 0, pointer-events: none, dont le fond est radial-gradient(circle at var(--shine-x, 50%) var(--shine-y, 50%), rgba(255,255,255,.12) 0%, transparent 60%). À chaque mouvement, le JS écrit --shine-x et --shine-y en pourcentage de la carte : la tache claire suit le pointeur. Au mouseenter, la transition est réglée sur box-shadow .3s ease seulement — la rotation suit donc le pointeur sans lissage ; au mouseleave, transform revient à perspective(600px) rotateX(0) rotateY(0) scale(1), l'ombre à none, et la transition passe à transform .5s cubic-bezier(.23, 1, .32, 1), box-shadow .5s ease : le retour prend 500 ms.

La perspective est portée par la chaîne transform elle-même : perspective(600px) en tête, à chaque écriture, y compris au retour. C'est ce qui donne le point de fuite — sans lui, une rotation 3D est projetée orthogonalement et la carte ne bascule pas, elle se comprime : mesuré à 13,7° sans perspective, sa boîte passe de 210 à 204 px de large (cos 13,7°) et garde 147 px de haut, un rétrécissement de 3 % sans trapèze ni profondeur. Avec une perspective de 600 px, la même carte mesure 154 px de haut au même angle : le bord proche grandit, le bord lointain rétrécit, la 3D apparaît. Porter la perspective dans le transform plutôt que sur le parent rend chaque carte autonome : son point de fuite est centré sur elle et ne dépend pas du conteneur où vous la posez.

Une promesse reste inerte : .tilt-icon et .tilt-label portent transform: translateZ(30px) et translateZ(20px), et la carte transform-style: preserve-3d, pour que le contenu flotte au-dessus de la face. Or la carte a aussi overflow: hidden, qui force transform-style: flat selon la spécification : les enfants sont aplatis dans le plan de la carte. Mesuré avec une perspective de 600 px : l'icône fait 20,2 px de large en overflow: hidden et 21,3 px en overflow: visible (× 1,053 = 600 / 570), preuve que le relief n'agit qu'une fois l'overflow retiré — mais c'est lui qui découpe le reflet aux coins arrondis. Le code choisit implicitement le reflet contre le relief.

Accessibilité

  • prefers-reduced-motion absent du code : l'inclinaison, le zoom de 5 % et le retour de 500 ms jouent quelle que soit la préférence. Le plus sûr est de ne pas poser les écouteurs quand matchMedia('(prefers-reduced-motion: reduce)').matches ; à défaut, @media (prefers-reduced-motion: reduce) { .tilt-card { transform: none !important; transition: none !important } } neutralise les écritures en ligne du JS.
  • L'effet est exclusivement souris et la carte est un div à cursor: pointer qui ne fait rien au clic : la main promise n'a pas d'action derrière. Si la carte mène quelque part, faites-en un a ou un button et donnez-lui un :focus-visible — au clavier, aucune inclinaison ne signalera le focus, ce qui est acceptable tant que le focus est visible autrement.
  • Contraste : le libellé rgba(255,255,255,.7) en 13,6 px mesure 8,6:1 sur #1e1e3a et 7,5:1 sur #2a2a4e, 5,6:1 sous la tache du reflet (blanc à 12 %) — AA partout, AAA hors reflet. Le pointeur natif reste visible (aucun cursor: none).
  • Lecteurs d'écran : .tilt-shine est un div vide sans rôle — inoffensif, mais ajoutez aria-hidden="true" pour que rien ne le liste. Les transformations n'altèrent pas l'arbre d'accessibilité : l'icône (un émoji, lu par son nom Unicode — donnez-lui aria-hidden ou un aria-label) et le libellé restent lus tels quels.
  • Intégration : la perspective est déjà en tête de la chaîne transform (perspective(600px)), rien à ajouter sur le parent ; scale(1.05) fait déborder la carte de 5 px de chaque côté sur ses voisines — prévoyez une marge ou un z-index au survol ; will-change: transform réserve un calque de composition par carte, à retirer au-delà d'une dizaine d'instances. La remise à zéro à la sortie est faite par le code (transform, ombre, transition).

Compatibilité navigateur

Code ES5 (var, function). Le CSS requiert les transformations 3D (rotateX/rotateY), les propriétés personnalisées (var()) pour le reflet et la propriété raccourcie inset, qui dimensionne le calque de reflet. Zéro dépendance.

Chrome 87+✓ Complet
Firefox 66+✓ Complet
Safari 14.1+✓ Complet
Edge 87+✓ Complet
Mobile iOS 14.5+✓ Statique (pas de survol)
Android Chrome✓ Statique (pas de survol)

Sans JavaScript, la carte est statique avec un reflet centré (les variables retombent sur 50 %). Sans inset (Safari avant 14.1, Firefox avant 66), le calque de reflet n'a ni taille ni position : plus de tache claire, l'inclinaison et l'ombre restent. Sans var() (navigateurs de 2016), le dégradé est invalide et le reflet disparaît de même.

Le code

Structure HTML à coller dans votre page (CSS + JS disponibles avec un compte premium) :

index.html — structure
<div class="tilt-card" data-tilt-card>
  <div class="tilt-shine"></div>
  <div class="tilt-icon">⚖</div>
  <div class="tilt-label">Tilt me</div>
</div>
🔒 Débloquer le code complet — à partir de 2,99 € le 1er mois

HTML + CSS + JS complet, prêt à coller — avec des centaines d'effets premium.

Personnaliser

Options passées à l'API ou attributs data-* :

Option / propriétéDéfautEffet
perspective (JS — perspective(600px) en tête du transform) 600 px Point de fuite de la bascule, écrit dans la chaîne transform au mousemove et au retour : modifiez la valeur aux deux endroits. Plus elle est petite, plus la 3D est forte : 400px = très marqué, 1200px = discret. Une perspective posée en plus sur le parent se cumulerait avec celle-ci.
Amplitude (JS — × −15 et × 15) ±15° aux bords Angle maximal atteint quand le pointeur touche un bord. 8 pour une carte large ou du texte dense ; 25 pour un objet ludique. Les deux coefficients peuvent différer (moins de rotateX sur une carte haute).
Zoom au survol (JS — scale(1.05)) 1.05 Agrandissement pendant le survol, 5 px de chaque côté sur 200 px. 1 supprime le débordement sur les voisines ; 1.08 accentue la mise en avant.
Ombre au survol (JS — 0 20px 40px rgba(99,102,241,.3)) indigo, 40 px de flou, décalée de 20 px Ombre écrite au mousemove, retirée au mouseleave (none). Une ombre neutre rgba(0,0,0,.45) convient à des visuels ; le décalage vertical suggère la hauteur de la carte.
Retour (JS — transform 0.5s cubic-bezier(0.23,1,0.32,1)) .5s, courbe ease-out prononcée Durée et courbe du retour à plat à la sortie. .25s = carte nerveuse ; .8s avec cubic-bezier(.34, 1.56, .64, 1) = léger rebond au repos.
Reflet (CSS — rgba(255,255,255,.12) 0%, transparent 60%) blanc à 12 %, rayon 60 % Intensité et étendue de la tache claire. .2 sur des visuels sombres ; transparent 40% pour un reflet plus ponctuel. Supprimez le calque .tilt-shine pour un tilt sec.
Taille et face (CSS — .tilt-card 200 × 140, dégradé 145deg) 200 × 140 px, #1e1e3a → #2a2a4e La géométrie est libre : les angles sont calculés en proportion de la largeur et de la hauteur réelles. Sur une grande carte, baissez l'amplitude ; le dégradé peut céder la place à une image de fond, le reflet se pose dessus.

FAQ

En tête de la chaîne transform écrite par le script : 'perspective(600px) rotateX(' + …, au mousemove comme au retour du mouseleave — l'ordre compte, la perspective doit être la première fonction. Sans elle, le navigateur projetterait la rotation orthogonalement : bord proche et bord lointain garderaient la même taille, la carte se contenterait de se rétrécir (mesuré : 204 px de large au lieu de 210 à 13,7°, hauteur inchangée) ; avec, la même rotation produit un trapèze de 154 px de haut. La porter dans le transform plutôt qu'en perspective: 600px sur le parent rend la carte autonome — le point de fuite est centré sur elle et ne dépend pas de son conteneur — au prix d'une valeur à modifier à deux endroits dans le JS. Si vous préférez la propriété sur le parent, retirez la fonction des deux chaînes pour ne pas cumuler.
Parce que la carte a overflow: hidden. La spécification impose qu'un overflow autre que visible force transform-style: flat : le preserve-3d déclaré est ignoré et les enfants sont aplatis dans le plan de la carte, translateZ compris. Mesuré avec une perspective de 600 px : l'icône mesure 20,2 px en overflow: hidden et 21,3 px en overflow: visible, soit le grossissement attendu de 600 / (600 − 30). Le dilemme : overflow: hidden est ce qui découpe le reflet aux coins arrondis. Pour avoir les deux, retirez l'overflow de la carte et donnez au calque .tilt-shine son propre border-radius: 16px (ou un clip-path: inset(0 round 16px)).
Le script fait querySelectorAll('[data-tilt-card]') : chaque carte a ses propres écouteurs et son propre reflet (card.querySelector('.tilt-shine'), facultatif). Pour une carte cliquable, remplacez le div par un a en display: flex — le code ne dépend pas de la balise. Deux précautions : scale(1.05) fait déborder la carte survolée sur ses voisines, donnez-lui position: relative; z-index: 1 au survol ou une marge de 6 px ; et will-change: transform crée un calque de composition par carte, à supprimer si la grille en compte des dizaines. Au tactile, aucun mousemove : la carte reste plate et cliquable.