Qu'est-ce qu'un bon effet au survol, et fonctionne-t-il sur mobile ?

Un effet au survol donne un retour immédiat quand le pointeur atteint un élément : agrandissement, changement de couleur, soulignement, reflet ou révélation. Il se construit avec des transitions CSS sur :hover et :focus-visible, des pseudo-éléments pour les soulignements et les reflets, et un peu de JavaScript pour la physique ou l'inclinaison 3D qui suit le pointeur.

Comment choisir, performances et accessibilité
  • Choisissez des effets sobres comme Underline Expand, Color Shift ou Scale & Rotate pour la navigation et les cartes, et des effets ludiques comme Like Heart, Paint Brush ou Gravity Simulation pour les moments de surprise.
  • Les écrans tactiles n'ont pas de survol : placez les styles de survol dans @media (hover: hover), ne cachez jamais une information essentielle derrière un survol et reproduisez chaque effet au focus clavier.
  • Scale & Rotate, Background Slide, Like Heart, Paint Brush et Gravity Simulation sont gratuits avec leur code complet ; les autres sont inclus dans Premium.
Scale & Rotate

Scale & Rotate

Agrandissement avec légère rotation.

CSS Transform
Voir le code source
HTML
<div class="demo-preview">
  <div class="hover-box hover-scale-rotate">
    <span>Scale & Rotate</span>
  </div>
</div>
CSS
.demo-preview,
.demo-preview *,
.demo-preview *::before,
.demo-preview *::after {
  box-sizing: border-box
}

.demo-preview {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 250px;
  overflow: hidden;
  position: relative;
  background: rgb(26, 26, 36);
  color: rgb(255, 255, 255);
  font-family: sans-serif;
}

/* Variables du design system du site, dont ce code dépend (valeurs de css/style.css — à adapter librement). */
.demo-preview {
  --bg-card: #12121a;
  --border: rgba(255, 255, 255, 0.08);
  --radius-xl: 1rem;
}

.hover-box {
  width: 200px;
  height: 200px;
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius-xl);
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 600;
  cursor: pointer;
  position: relative;
  overflow: hidden;
}

.hover-scale-rotate {
  transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}

.hover-scale-rotate:hover {
  transform: scale(1.1) rotate(5deg);
  box-shadow: 0 20px 40px rgba(99, 102, 241, 0.3);
}

.hover-box span {
  position: relative;
  z-index: 1;
}
JavaScript
/* CSS pur — aucun JavaScript requis */
BG Slide

Background Slide

Le fond glisse de gauche à droite.

CSS Background
Voir le code source
HTML
<div class="demo-preview">
  <div class="hover-box hover-bg-slide">
    <span>BG Slide</span>
  </div>
</div>
CSS
.demo-preview,
.demo-preview *,
.demo-preview *::before,
.demo-preview *::after {
  box-sizing: border-box
}

.demo-preview {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 250px;
  overflow: hidden;
  position: relative;
  background: rgb(26, 26, 36);
  color: rgb(255, 255, 255);
  font-family: sans-serif;
}

/* Variables du design system du site, dont ce code dépend (valeurs de css/style.css — à adapter librement). */
.demo-preview {
  --bg-card: #12121a;
  --border: rgba(255, 255, 255, 0.08);
  --radius-xl: 1rem;
}

.hover-box {
  width: 200px;
  height: 200px;
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius-xl);
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 600;
  cursor: pointer;
  position: relative;
  overflow: hidden;
}

.hover-bg-slide {
  background: linear-gradient(90deg, var(--bg-card) 50%, #6366f1 50%);
  background-size: 200% 100%;
  background-position: left;
  transition: all 0.5s ease;
}

.hover-bg-slide:hover {
  background-position: right;
  color: white;
}

.hover-box span {
  position: relative;
  z-index: 1;
}
JavaScript
/* CSS pur — aucun JavaScript requis */

Border Gradient

Bordure gradient qui apparaît au survol.

CSS Border

Shine Effect

Reflet lumineux qui traverse l'élément.

CSS Light

Underline Expand

Ligne qui s'étend depuis le centre.

CSS Line

Icon Bounce

L'icône rebondit au survol.

CSS Animation

Color Shift

Transition de couleur fluide.

CSS Color

Blur Reveal

L'élément flou devient net au survol.

CSS Filter

Shadow Inset

Ombre intérieure lumineuse.

CSS Shadow

Tilt 3D

Inclinaison perspective au survol.

CSS 3D

Shutter Reveal

Effet volets qui s'ouvrent au survol.

CSS Reveal

Glitch Hover

Effet glitch RGB au survol.

CSS Glitch

Fill Up

Remplissage de bas en haut avec courbe ease-quart. Inspiré de Daily Delights.

CSS Fill Bezier

Filter Animated

Animation de filtres CSS optimisée GPU avec will-change.

CSS Filter GPU

Spotlight Effect

Effet projecteur qui suit le curseur sur l'élément.

JS Mouse Light

Magnetic Pull

L'élément est attiré par le curseur avec effet magnétique.

JS Mouse Physics

Elastic Scale

Agrandissement avec courbe élastique et rebond naturel.

CSS Bezier Scale

Gradient Shift

Gradient animé qui accélère au survol pour un effet dynamique.

CSS Gradient Animation
423 likes

Like Heart

Bouton like avec animation pop, anneau de propagation et explosion de particules colorées au clic.

CSS Animation Interactive
Voir le code source
HTML
<div class="demo-preview">
  <div class="mi-heart-scene">
    <button class="mi-heart-btn" onclick="this.classList.toggle('liked')">
      <svg viewBox="0 0 24 24"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>
      <div class="mi-heart-particles">
        <span class="mi-heart-particle"></span>
        <span class="mi-heart-particle"></span>
        <span class="mi-heart-particle"></span>
        <span class="mi-heart-particle"></span>
        <span class="mi-heart-particle"></span>
        <span class="mi-heart-particle"></span>
        <span class="mi-heart-particle"></span>
        <span class="mi-heart-particle"></span>
      </div>
    </button>
    <span class="mi-heart-count">423 likes</span>
  </div>
</div>
CSS
.demo-preview,
.demo-preview *,
.demo-preview *::before,
.demo-preview *::after {
  box-sizing: border-box
}

.demo-preview {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 250px;
  overflow: hidden;
  position: relative;
  background: rgb(26, 26, 36);
  color: rgb(255, 255, 255);
  font-family: sans-serif;
}

.mi-heart-scene {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 20px;
}

.mi-heart-btn {
  width: 80px;
  height: 80px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.05);
  border: 2px solid rgba(255, 255, 255, 0.1);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
  transition: all 0.3s ease;
}

.mi-heart-btn:hover {
  background: rgba(239, 68, 68, 0.1);
  border-color: rgba(239, 68, 68, 0.3);
}

.mi-heart-btn svg {
  width: 36px;
  height: 36px;
  stroke: rgba(255, 255, 255, 0.5);
  fill: none;
  stroke-width: 2;
  transition: all 0.3s ease;
}

.mi-heart-particles {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.mi-heart-particle {
  position: absolute;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  top: 50%;
  left: 50%;
  opacity: 0;
}

.mi-heart-particle:nth-child(1) {
  background: #ef4444;
  --angle: 0deg;
}

.mi-heart-particle:nth-child(2) {
  background: #f97316;
  --angle: 45deg;
}

.mi-heart-particle:nth-child(3) {
  background: #eab308;
  --angle: 90deg;
}

.mi-heart-particle:nth-child(4) {
  background: #ef4444;
  --angle: 135deg;
}

.mi-heart-particle:nth-child(5) {
  background: #ec4899;
  --angle: 180deg;
}

.mi-heart-particle:nth-child(6) {
  background: #f97316;
  --angle: 225deg;
}

.mi-heart-particle:nth-child(7) {
  background: #eab308;
  --angle: 270deg;
}

.mi-heart-particle:nth-child(8) {
  background: #ec4899;
  --angle: 315deg;
}

.mi-heart-count {
  font-size: 0.85rem;
  font-weight: 600;
  color: rgba(255, 255, 255, 0.5);
  transition: all 0.3s ease;
}
JavaScript
(function() {
  function toggle() {
    open = !open;
    gift.classList.toggle('guw-open', open);
    scene.classList.toggle('guw-done', open);
    gift.setAttribute('aria-label', open ? 'Cadeau ouvert (cliquer pour ré-emballer)' : 'Ouvrir le cadeau');
    if (open) setTimeout(burst, reduceMotion ? 0 : 1100);
  }
})();

Bookmark Toggle

Bouton de sauvegarde avec animation de chute, remplissage du fond et transition de couleur.

CSS Toggle Animation

Submit Button

Bouton envoi avec morphing circulaire, spinner de chargement et animation de check de succès.

CSS JS States

Ripple Click

Effet Material Design ripple au clic. L'onde se propage depuis le point de contact et s'estompe.

CSS JS Material

Pull to Refresh

Simulation pull-to-refresh mobile avec indicateur rotatif, déplacement du contenu et spinner de chargement.

CSS JS Mobile

Notification Bell

Cloche avec animation de vibration, badge compteur avec pop-in et toast de notification glissant.

CSS JS Notification

Star Rating

Système de notation avec animation pop et rotation sur chaque étoile. Labels dynamiques selon la note.

CSS JS Rating

Switch with Icons

Interrupteurs avec icônes intégrées dans la poignée, animation bounce et changement de couleur.

CSS Toggle Icons

Copy to Clipboard

Bouton copier avec transition d'icônes (copie → check), couleur de succès et tooltip « Copié ».

CSS JS Clipboard

Confetti Button

Bouton qui déclenche une pluie de confettis colorés avec gravité et rotation réalistes.

JS Canvas Animation
Dessinez avec la souris

Paint Brush

Pinceau lumineux qui laisse une traînée colorée.

Canvas Draw Creative
Voir le code source
HTML
<div class="demo-preview">
  <canvas id="paintCanvas" class="interactive-zone paint-canvas"></canvas>
  <span class="info-label">Dessinez avec la souris</span>
</div>
CSS
.demo-preview,
.demo-preview *,
.demo-preview *::before,
.demo-preview *::after {
  box-sizing: border-box
}

.demo-preview {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 250px;
  overflow: hidden;
  position: relative;
  background: rgb(26, 26, 36);
  color: rgb(255, 255, 255);
  font-family: sans-serif;
}

/* Variables du design system du site, dont ce code dépend (valeurs de css/style.css — à adapter librement). */
.demo-preview {
  --text-muted: #8b8b93;
}

.paint-canvas {
  background: #0a0a0f;
}

.interactive-zone {
  width: 100%;
  height: 300px;
  position: relative;
  cursor: none;
}

.info-label {
  position: absolute;
  bottom: 10px;
  left: 50%;
  transform: translateX(-50%);
  font-size: 0.75rem;
  color: var(--text-muted);
  pointer-events: none;
  z-index: 10;
}
JavaScript
(function() {
  // ========== INTERACTIF ==========
  // 1. Paint Brush
  (function() {
    const paintCanvas = document.getElementById('paintCanvas');
    if (!paintCanvas) return;
    const paintCtx = paintCanvas.getContext('2d');
    paintCanvas.width = paintCanvas.offsetWidth;
    paintCanvas.height = 300;

    let painting = false;
    let hue = 0;

    paintCanvas.addEventListener('mousedown', () => painting = true);
    paintCanvas.addEventListener('mouseup', () => painting = false);
    paintCanvas.addEventListener('mousemove', (e) => {
      if (!painting) return;
      const rect = paintCanvas.getBoundingClientRect();
      const x = e.clientX - rect.left;
      const y = e.clientY - rect.top;

      paintCtx.beginPath();
      paintCtx.arc(x, y, 15, 0, Math.PI * 2);
      paintCtx.fillStyle = `hsl(${hue}, 70%, 60%)`;
      paintCtx.fill();
      hue = (hue + 2) % 360;
    });

    // Fade effect
    const paintFadeInterval = setInterval(() => {
      paintCtx.fillStyle = 'rgba(10, 10, 15, 0.02)';
      paintCtx.fillRect(0, 0, paintCanvas.width, paintCanvas.height);
    }, 50);
  })();
})();

Repel Particles

Particules qui fuient le curseur.

JS Particles Physics

Text Scramble

Texte qui se décode au survol.

JS Text Decode

Elastic Drag

Élément draggable avec retour élastique.

JS Drag Spring

Scratch Reveal

Grattez pour révéler le contenu caché.

Canvas Reveal Game

Morph on Click

Forme qui se transforme à chaque clic.

CSS JS Transform

Live Typing

Chaque caractère apparaît avec animation.

JS Keyboard Animation

Sound Visualizer

Barres qui réagissent dynamiquement.

JS Audio Visual

Magnetic Letters

Lettres attirées ou repoussées par le curseur.

JS Physics Text

Gravity Drag

Éléments qui tombent avec gravité une fois relâchés.

JS Physics Drag
Cliquez pour ajouter des balles

Gravity Simulation

Balles soumises à la gravité avec rebond.

JS Physics Interactive
Voir le code source
HTML
<div class="demo-preview">
  <div class="physics-container" id="gravityContainer">
    <span class="info-label">Cliquez pour ajouter des balles</span>
  </div>
</div>
CSS
.demo-preview,
.demo-preview *,
.demo-preview *::before,
.demo-preview *::after {
  box-sizing: border-box
}

.demo-preview {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 250px;
  overflow: hidden;
  position: relative;
  background: rgb(26, 26, 36);
  color: rgb(255, 255, 255);
  font-family: sans-serif;
}

/* Variables du design system du site, dont ce code dépend (valeurs de css/style.css — à adapter librement). */
.demo-preview {
  --text-muted: #8b8b93;
}

.physics-gravity-ball {
  position: absolute;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  cursor: grab;
}

.physics-gravity-ball:active {
  cursor: grabbing;
}

.info-label {
  position: absolute;
  bottom: 10px;
  left: 50%;
  transform: translateX(-50%);
  font-size: 0.75rem;
  color: var(--text-muted);
  pointer-events: none;
  z-index: 10;
}

.physics-container {
  width: 100%;
  height: 300px;
  position: relative;
}
JavaScript
(function() {
  // ========== PHYSIQUE ==========
  // 1. Gravity Balls
  (function() {
    const gravityContainer = document.getElementById('gravityContainer');
    if (!gravityContainer) return;
    const gravityBalls = [];
    const gravity = 0.5;
    const bounce = 0.7;
    const friction = 0.99;

    gravityContainer.addEventListener('click', (e) => {
      const rect = gravityContainer.getBoundingClientRect();
      const ball = document.createElement('div');
      ball.className = 'physics-gravity-ball';
      ball.style.background = `hsl(${Math.random() * 360}, 70%, 60%)`;
      ball.style.left = (e.clientX - rect.left - 15) + 'px';
      ball.style.top = (e.clientY - rect.top - 15) + 'px';
      gravityContainer.appendChild(ball);
      gravityBalls.push({
        el: ball,
        x: e.clientX - rect.left - 15,
        y: e.clientY - rect.top - 15,
        vx: (Math.random() - 0.5) * 10,
        vy: 0
      });
      if (gravityBalls.length > 20) {
        const old = gravityBalls.shift();
        old.el.remove();
      }
    });

    function animateGravity() {
      const rect = gravityContainer.getBoundingClientRect();
      gravityBalls.forEach(ball => {
        ball.vy += gravity;
        ball.vx *= friction;
        ball.x += ball.vx;
        ball.y += ball.vy;

        if (ball.y > rect.height - 30) {
          ball.y = rect.height - 30;
          ball.vy *= -bounce;
        }
        if (ball.x < 0) {
          ball.x = 0;
          ball.vx *= -bounce;
        }
        if (ball.x > rect.width - 30) {
          ball.x = rect.width - 30;
          ball.vx *= -bounce;
        }

        ball.el.style.left = ball.x + 'px';
        ball.el.style.top = ball.y + 'px';
      });
      requestAnimationFrame(animateGravity);
    }
    animateGravity();
  })();
})();

Elastic String

Corde élastique qui réagit au curseur.

JS Elastic Mouse

Magnetic Field

Particules attirées par un champ magnétique.

JS Magnetic Particles

Newton's Cradle

Pendules avec transfert d'énergie cinétique.

JS Pendulum Energy

Liquid Simulation

Gouttes qui se combinent et se séparent.

JS Fluid Metaball

Spring Physics

Animation élastique avec effet ressort.

JS Spring Bounce

Collision Detection

Balles qui rebondissent les unes sur les autres.

JS Collision Physics

Cloth Simulation

Simulation de tissu interactif.

Canvas Cloth Interactive

Newton's Cradle

Pendule de Newton avec transfert d'énergie.

Canvas Physics Momentum

Rope Physics

Corde interactive avec simulation physique.

Canvas Verlet Draggable

Fluid X-Ray Reveal

Le curseur « scanne » et révèle une couche cachée — ici, la même personne déguisée. Masque fluide à traînée. Avant/après, normal/déguisé, jour/nuit : la 2e couche peut être n'importe quoi.

💡 Idéal pour : avant/après, « voir le costume », teaser de soirée Halloween, comparaison de visuels, easter-egg de portrait.

CSS Mask Vanilla JS Hover Premium

Scratch Coupon Reveal

Carte à gratter qui dévoile un code promo, puis propose de le copier. Interaction tactile et ludique qui valorise l'offre. Code et seuil de révélation personnalisables.

💡 Idéal pour : code promo, jeu-concours, fidélité, Black Friday, pop-up d'offre, gamification e-commerce.

Canvas Scratch Coupon Premium

Gift Unwrap Reveal

Un cadeau qui tourne sur lui-même, dont le couvercle se soulève et d'où sort un code promo (ou tout contenu) en rebond, avec étincelles. Idéal pour dévoiler une offre de fête de façon mémorable. Code personnalisable.

💡 Idéal pour : code promo de Noël, révélation d'offre, jeu-concours, message de vœux, calendrier surprise, e-commerce festif.

Vanilla JS Reveal Festif Gratuit
Voir le code source
HTML
<div class="demo-preview">
  <div class="guw-scene" data-guw data-code="NOEL30">
    <button type="button" class="guw-gift" data-guw-box aria-label="Ouvrir le cadeau">
      <span class="guw-inner" aria-hidden="true">
        <span class="guw-code" data-guw-code>🎄 NOEL30</span>
        <span class="guw-box"></span>
        <span class="guw-lid"></span>
        <span class="guw-bow">🎀</span>
      </span>
    </button>
    <div class="guw-hint" aria-hidden="true">Clique pour déballer</div>
  </div>
</div>
CSS
.demo-preview,
.demo-preview *,
.demo-preview *::before,
.demo-preview *::after {
  box-sizing: border-box
}

.demo-preview {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  min-height: 200px;
  position: relative;
  background: rgb(26, 26, 36);
  color: rgb(255, 255, 255);
  font-family: sans-serif;
}

.guw-scene {
  position: relative;
  display: grid;
  place-items: center;
  min-height: 330px;
  border-radius: 14px;
  overflow: hidden;
  background: radial-gradient(circle at 50% 35%, #241433, #0b0a14 72%);
}

.guw-gift {
  position: relative;
  width: 220px;
  height: 250px;
  border: 0;
  background: none;
  cursor: pointer;
  outline-offset: 8px;
}

.guw-gift:focus-visible {
  outline: 2px solid #fb7185;
  border-radius: 12px;
}

.guw-code {
  position: absolute;
  left: 50%;
  bottom: 58px;
  transform: translateX(-50%) translateY(0) scale(.6);
  z-index: 3;
  background: linear-gradient(135deg, #ffe08a, #f2a900);
  color: #5b3a00;
  font-weight: 800;
  letter-spacing: .06em;
  font-size: 16px;
  padding: 9px 16px;
  border-radius: 10px;
  white-space: nowrap;
  box-shadow: 0 8px 18px rgba(0, 0, 0, .4);
  border: 2px dashed #b97e00;
  transition: transform .55s cubic-bezier(.2, 1.55, .35, 1);
  transition-delay: 0s;
}

.guw-box {
  position: absolute;
  left: 50%;
  bottom: 40px;
  transform: translateX(-50%);
  width: 140px;
  height: 96px;
  border-radius: 9px;
  z-index: 4;
  background: linear-gradient(135deg, #e01e37, #9c0f20);
  box-shadow: inset 0 -10px 16px rgba(0, 0, 0, .32), 0 14px 22px rgba(0, 0, 0, .4);
}

.guw-box::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 0;
  transform: translateX(-50%);
  width: 24px;
  height: 100%;
  background: linear-gradient(#ffd166, #f2a900);
}

.guw-box::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 0;
  transform: translateY(-50%);
  width: 100%;
  height: 24px;
  background: linear-gradient(90deg, #f2a900, #ffd166);
}

.guw-lid {
  position: absolute;
  left: 50%;
  bottom: 124px;
  transform: translateX(-50%) translateY(0) rotate(0);
  width: 162px;
  height: 40px;
  border-radius: 9px;
  z-index: 6;
  background: linear-gradient(135deg, #f0263f, #b01225);
  box-shadow: 0 6px 12px rgba(0, 0, 0, .35);
  transition: transform .5s cubic-bezier(.34, 1.3, .5, 1);
  transition-delay: .55s;
}

.guw-lid::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 0;
  transform: translateX(-50%);
  width: 24px;
  height: 100%;
  background: linear-gradient(#ffd166, #f2a900);
}

.guw-bow {
  position: absolute;
  left: 50%;
  bottom: 150px;
  transform: translateX(-50%) translateY(0);
  font-size: 40px;
  z-index: 7;
  transition: transform .5s cubic-bezier(.34, 1.3, .5, 1);
  transition-delay: .55s;
}

.guw-inner {
  position: absolute;
  inset: 0;
  transform-origin: 50% 70%;
  transition: transform .75s cubic-bezier(.45, .05, .2, 1);
  transition-delay: .9s;
}

.guw-gift.guw-open .guw-inner {
  transform: rotate(360deg);
  transition-delay: 0s;
}

.guw-gift.guw-open .guw-lid {
  transform: translateX(-50%) translateY(-86px) rotate(-10deg);
  transition-delay: .78s;
}

.guw-gift.guw-open .guw-bow {
  transform: translateX(-50%) translateY(-104px) rotate(28deg);
  transition-delay: .78s;
}

.guw-gift.guw-open .guw-code {
  transform: translateX(-50%) translateY(-104px) scale(1);
  transition-delay: 1s;
}

.guw-spark {
  position: absolute;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: #ffd166;
  z-index: 8;
  transform: translate(-50%, -50%);
  pointer-events: none;
  animation: guw-spark .75s ease-out forwards;
}

.guw-hint {
  position: absolute;
  bottom: 12px;
  left: 50%;
  transform: translateX(-50%);
  font-size: 12px;
  color: #fb7185;
  opacity: .6;
  pointer-events: none;
  transition: opacity .3s;
}

.guw-scene.guw-done .guw-hint {
  opacity: 0;
}

@media (prefers-reduced-motion: reduce) {

  .guw-inner,
  .guw-lid,
  .guw-bow,
  .guw-code {
    transition: none !important;
  }
}

@keyframes guw-spark {
  to {
    transform: translate(-50%, -50%) translate(var(--dx), var(--dy)) scale(.2);
    opacity: 0;
  }
}
JavaScript
(function() {
  /* Sprint décembre 2026 — Noël — Gift Unwrap Reveal (fx-0812) */
  (function() {
    var reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

    document.querySelectorAll('[data-guw]').forEach(function(scene) {
      var gift = scene.querySelector('[data-guw-box]');
      var code = scene.querySelector('[data-guw-code]');
      if (scene.dataset.code) code.textContent = '🎄 ' + scene.dataset.code;
      var open = false;

      function burst() {
        if (reduceMotion) return;
        for (var i = 0; i < 12; i++) {
          var s = document.createElement('span');
          s.className = 'guw-spark';
          s.style.left = '50%';
          s.style.bottom = '150px';
          var a = (i / 12) * 6.2832 + Math.random() * 0.4,
            d = 50 + Math.random() * 50;
          s.style.setProperty('--dx', Math.cos(a) * d + 'px');
          s.style.setProperty('--dy', -Math.abs(Math.sin(a) * d) - 10 + 'px');
          gift.appendChild(s);
          (function(el) {
            setTimeout(function() {
              el.remove();
            }, 800);
          })(s);
        }
      }

      function toggle() {
        open = !open;
        gift.classList.toggle('guw-open', open);
        scene.classList.toggle('guw-done', open);
        gift.setAttribute('aria-label', open ? 'Cadeau ouvert (cliquer pour ré-emballer)' : 'Ouvrir le cadeau');
        if (open) setTimeout(burst, reduceMotion ? 0 : 1100);
      }
      gift.addEventListener('click', toggle);
      gift.addEventListener('keydown', function(e) {
        if (e.key === 'Enter' || e.key === ' ') {
          e.preventDefault();
          toggle();
        }
      });
    });
  })();
})();

Questions fréquentes

Quel est le meilleur effet hover pour une carte ? +

Le lift effect (translateY(-8px) + box-shadow) est le plus populaire et le plus efficace. Il crée une sensation de profondeur sans être distrayant. Ajoutez une transition de border-color pour renforcer le feedback visuel.

Comment créer un underline animé au hover ? +

Utilisez un pseudo-élément ::after avec position:absolute, bottom:0, height:2px, et width:0. Au hover, animez width vers 100%. Pour un effet depuis le centre : left:50%, transform:translateX(-50%), animez width.

Comment faire un effet tilt 3D au hover ? +

Écoutez mousemove sur la carte. Calculez la position du curseur relative au centre. Appliquez transform: perspective(1000px) rotateX(Xdeg) rotateY(Ydeg) avec les angles calcules. Ajoutez une transition pour la fluidité.

Les effets hover fonctionnent-ils sur mobile ? +

Non — il n'y a pas de hover sur tactile. Utilisez @media (hover: hover) pour cibler uniquement les appareils avec pointeur. Sur mobile, remplacez par des effets au clic/tap avec :active ou JavaScript.

Comment animer le hover d'une image ? +

Les effets les plus populaires : zoom (transform: scale(1.1) dans un overflow:hidden), overlay qui apparait (pseudo-élément avec opacity), filtre (grayscale vers couleur), et clip-path qui révèle du contenu caché.