Hover & Interactions
Hover effects, micro-interactions, interactive elements and physics.
Updated
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 up with a slight rotation.
View source code
<div class="demo-preview">
<div class="hover-box hover-scale-rotate">
<span>Scale & Rotate</span>
</div>
</div>.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;
}/* Pure CSS — no JavaScript required */Background Slide
The background slides from left to right.
View source code
<div class="demo-preview">
<div class="hover-box hover-bg-slide">
<span>BG Slide</span>
</div>
</div>.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;
}/* Pure CSS — no JavaScript required */Border Gradient
Gradient border that appears on hover.
Shine Effect
Light reflection that sweeps across the element.
Underline Expand
Line that expands from the center.
Icon Bounce
The icon bounces on hover.
Color Shift
Smooth color transition.
Blur Reveal
The blurred element becomes sharp on hover.
Shadow Inset
Glowing inner shadow.
Tilt 3D
Perspective tilt on hover.
Shutter Reveal
Shutter effect that opens on hover.
Glitch Hover
RGB glitch effect on hover.
Fill Up
Bottom-to-top fill with ease-quart curve. Inspired by Daily Delights.
Filter Animated
GPU-optimized CSS filter animation with will-change.
Spotlight Effect
Spotlight effect that follows the cursor on the element.
Magnetic Pull
The element is attracted to the cursor with a magnetic effect.
Elastic Scale
Scale up with elastic curve and natural bounce.
Gradient Shift
Animated gradient that speeds up on hover for a dynamic effect.
Like Heart
Like button with pop animation, propagation ring and colorful particle burst on click.
View source code
<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>.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;
}(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.
Submit Button
Submit button with circular morphing, loading spinner and success check animation.
Ripple Click
Material Design ripple effect on click. The wave spreads from the contact point and fades out.
Pull to Refresh
Mobile pull-to-refresh simulation with rotating indicator, content displacement and loading spinner.
Notification Bell
Bell with shake animation, counter badge with pop-in and sliding notification toast.
Star Rating
Rating system with pop and rotation animation on each star. Dynamic labels based on the rating.
Switch with Icons
Switches with icons embedded in the knob, bounce animation and color change.
Copy to Clipboard
Copy button with icon transition (copy → check), success color and "Copied" tooltip.
Confetti Button
Button that triggers a colorful confetti burst with realistic gravity and rotation.
Paint Brush
Glowing brush that leaves a colorful trail.
View source code
<div class="demo-preview">
<canvas id="paintCanvas" class="interactive-zone paint-canvas"></canvas>
<span class="info-label">Draw with your mouse</span>
</div>.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;
}(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.
Text Scramble
Text that decodes on hover.
Elastic Drag
Draggable element with elastic return.
Scratch Reveal
Scratch to reveal the hidden content.
Morph on Click
Shape that transforms on each click.
Live Typing
Each character appears with animation.
Sound Visualizer
Bars that react dynamically.
Magnetic Letters
Letters attracted to or repelled by the cursor.
Gravity Drag
Elements that fall with gravity once released.
Gravity Simulation
Balls subjected to gravity with bounce.
View source code
<div class="demo-preview">
<div class="physics-container" id="gravityContainer">
<span class="info-label">Click to add balls</span>
</div>
</div>.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;
}(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.
Magnetic Field
Particles attracted by a magnetic field.
Newton's Cradle
Pendulums with kinetic energy transfer.
Liquid Simulation
Drops that merge and separate.
Spring Physics
Elastic animation with spring effect.
Collision Detection
Balls that bounce off each other.
Cloth Simulation
Interactive cloth simulation.
Newton's Cradle
Newton's cradle with energy transfer.
Rope Physics
Interactive rope with physics simulation.
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.
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.
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.
View source code
<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>.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;
}
}(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();
}
});
});
})();
})();