What makes a good hover effect, and does it work on mobile?

A hover effect gives immediate feedback when the pointer reaches an element: scale, colour shift, underline, shine or reveal. It is built with CSS transitions on :hover and :focus-visible, pseudo-elements for underlines and shines, and small JavaScript additions for physics or a 3D tilt that follows the pointer.

How to choose, performance and accessibility
  • Pick understated effects such as Underline Expand, Color Shift or Scale & Rotate for navigation and cards, and playful ones like Like Heart, Paint Brush or Gravity Simulation for moments of delight.
  • Touch screens have no hover, so wrap hover-only styles in @media (hover: hover), never hide essential information behind a hover, and mirror every effect on keyboard focus.
  • Scale & Rotate, Background Slide, Like Heart, Paint Brush and Gravity Simulation are free with their full code; the rest come with Premium.
Scale & Rotate

Scale & Rotate

Scale up with a slight rotation.

CSS Transform
View source code
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
/* Pure CSS — no JavaScript required */
BG Slide

Background Slide

The background slides from left to right.

CSS Background
View source code
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
/* Pure CSS — no JavaScript required */

Border Gradient

Gradient border that appears on hover.

CSS Border

Shine Effect

Light reflection that sweeps across the element.

CSS Light

Underline Expand

Line that expands from the center.

CSS Line

Icon Bounce

The icon bounces on hover.

CSS Animation

Color Shift

Smooth color transition.

CSS Color

Blur Reveal

The blurred element becomes sharp on hover.

CSS Filter

Shadow Inset

Glowing inner shadow.

CSS Shadow

Tilt 3D

Perspective tilt on hover.

CSS 3D

Shutter Reveal

Shutter effect that opens on hover.

CSS Reveal

Glitch Hover

RGB glitch effect on hover.

CSS Glitch

Fill Up

Bottom-to-top fill with ease-quart curve. Inspired by Daily Delights.

CSS Fill Bezier

Filter Animated

GPU-optimized CSS filter animation with will-change.

CSS Filter GPU

Spotlight Effect

Spotlight effect that follows the cursor on the element.

JS Mouse Light

Magnetic Pull

The element is attracted to the cursor with a magnetic effect.

JS Mouse Physics

Elastic Scale

Scale up with elastic curve and natural bounce.

CSS Bezier Scale

Gradient Shift

Animated gradient that speeds up on hover for a dynamic effect.

CSS Gradient Animation
423 likes

Like Heart

Like button with pop animation, propagation ring and colorful particle burst on click.

CSS Animation Interactive
View source code
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

Save button with drop animation, background fill and color transition.

CSS Toggle Animation

Submit Button

Submit button with circular morphing, loading spinner and success check animation.

CSS JS States

Ripple Click

Material Design ripple effect on click. The wave spreads from the contact point and fades out.

CSS JS Material

Pull to Refresh

Mobile pull-to-refresh simulation with rotating indicator, content displacement and loading spinner.

CSS JS Mobile

Notification Bell

Bell with shake animation, counter badge with pop-in and sliding notification toast.

CSS JS Notification

Star Rating

Rating system with pop and rotation animation on each star. Dynamic labels based on the rating.

CSS JS Rating

Switch with Icons

Switches with icons embedded in the knob, bounce animation and color change.

CSS Toggle Icons

Copy to Clipboard

Copy button with icon transition (copy → check), success color and "Copied" tooltip.

CSS JS Clipboard

Confetti Button

Button that triggers a colorful confetti burst with realistic gravity and rotation.

JS Canvas Animation
Draw with your mouse

Paint Brush

Glowing brush that leaves a colorful trail.

Canvas Draw Creative
View source code
HTML
<div class="demo-preview">
  <canvas id="paintCanvas" class="interactive-zone paint-canvas"></canvas>
  <span class="info-label">Draw with your mouse</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

Particles that flee from the cursor.

JS Particles Physics

Text Scramble

Text that decodes on hover.

JS Text Decode

Elastic Drag

Draggable element with elastic return.

JS Drag Spring

Scratch Reveal

Scratch to reveal the hidden content.

Canvas Reveal Game

Morph on Click

Shape that transforms on each click.

CSS JS Transform

Live Typing

Each character appears with animation.

JS Keyboard Animation

Sound Visualizer

Bars that react dynamically.

JS Audio Visual

Magnetic Letters

Letters attracted to or repelled by the cursor.

JS Physics Text

Gravity Drag

Elements that fall with gravity once released.

JS Physics Drag
Click to add balls

Gravity Simulation

Balls subjected to gravity with bounce.

JS Physics Interactive
View source code
HTML
<div class="demo-preview">
  <div class="physics-container" id="gravityContainer">
    <span class="info-label">Click to add balls</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

Elastic string that reacts to the cursor.

JS Elastic Mouse

Magnetic Field

Particles attracted by a magnetic field.

JS Magnetic Particles

Newton's Cradle

Pendulums with kinetic energy transfer.

JS Pendulum Energy

Liquid Simulation

Drops that merge and separate.

JS Fluid Metaball

Spring Physics

Elastic animation with spring effect.

JS Spring Bounce

Collision Detection

Balls that bounce off each other.

JS Collision Physics

Cloth Simulation

Interactive cloth simulation.

Canvas Cloth Interactive

Newton's Cradle

Newton's cradle with energy transfer.

Canvas Physics Momentum

Rope Physics

Interactive rope with physics simulation.

Canvas Verlet Draggable

Fluid X-Ray Reveal

The cursor "scans" and reveals a hidden layer — here, the same person in costume. Fluid trailing mask. Before/after, normal/disguised, day/night: the 2nd layer can be anything.

💡 Perfect for: before/after, "see the costume", Halloween party teaser, visual comparison, portrait easter egg.

CSS Mask Vanilla JS Hover Premium

Scratch Coupon Reveal

A scratch card that reveals a promo code, then lets users copy it. Tactile, gamified interaction that showcases your offer. Code and reveal threshold are both customizable.

💡 Perfect for: promo codes, contests, loyalty programs, Black Friday, offer pop-ups, e-commerce gamification.

Canvas Scratch Coupon Premium

Gift Unwrap Reveal

A gift that spins on itself, lid lifting to reveal a promo code (or any content) with a bouncy spring animation and a burst of sparks. A memorable way to unveil a festive offer. Code is fully customizable.

💡 Perfect for: Christmas promo codes, offer reveals, contests, holiday greetings, surprise calendars, festive e-commerce.

Vanilla JS Reveal Festive Free
View source code
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="Open the gift">
      <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">Click to unwrap</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();
        }
      });
    });
  })();
})();

Frequently Asked Questions

What is the best hover effect for a card? +

The lift effect (translateY(-8px) + box-shadow) is the most popular and effective. It creates a sense of depth without being distracting. Add a border-color transition to enhance the visual feedback.

How to create an animated underline on hover? +

Use a ::after pseudo-element with position:absolute, bottom:0, height:2px, and width:0. On hover, animate width to 100%. For a center-out effect: left:50%, transform:translateX(-50%), animate width.

How to create a 3D tilt effect on hover? +

Listen for mousemove on the card. Calculate the cursor position relative to the center. Apply transform: perspective(1000px) rotateX(Xdeg) rotateY(Ydeg) with the calculated angles. Add a transition for smoothness.

Do hover effects work on mobile? +

No — there is no hover on touch devices. Use @media (hover: hover) to target only devices with a pointer. On mobile, replace with click/tap effects using :active or JavaScript.

How to animate an image on hover? +

The most popular effects: zoom (transform: scale(1.1) inside overflow:hidden), appearing overlay (pseudo-element with opacity), filter (grayscale to color), and clip-path revealing hidden content.