Where should the hostas go, in the shade?
Jardins Lemaire · landscaperMove the sun with the mouse: the shadows turn at the foot of the shrubs.
Three badges (◆ ★ ♥) sitting above their contact shadows: the pointer acts as the lamp, the ground shadows slide away from it and stretch, the badges' drop shadow turns and they lift slightly — one lighting for all three, with the mouse, a pen or a finger; when the user reduces motion, the badges stay put and only the shadows follow the light.
Updated
This effect is free — the 3D category contains 50 effects total, including 8 free. Effect.Labs has 840 vanilla effects. Explore the category →
Move the sun with the mouse: the shadows turn at the foot of the shrubs.
Atelier Roussel architects
HouseWorkshopBike shelter
Knight, tower, bishop: 28 mm seriesLead time 3 weeks
The root .fx-0029-root (250 px tall, rgb(26, 26, 36) background) holds .float-shadow-container, marked data-float-shadow: full width and 250 px tall, centering three .float-shadow-el spaced 30 px apart. Each carries a 60 × 60 px .float-shadow-shape badge (rounded indigo square, pink circle, rounded green square, all gradients) and, below it, a .float-shadow-shadow contact shadow: a 50 × 10 px black radial gradient ellipse at 35%, centered 15 px under the badge.
The script goes through every .fx-0029-root on the page and wires one instance per group. On each pointer move, it computes the light position relative to the container's center, from −1 to +1 on each axis, then applies the same lighting to all three elements: the ground shadow slides away from the pointer (up to 15 px) and stretches sideways when the light goes up or down (up to ×1.3) while fading; the badge gets a drop shadow blurred by 20 px and offset away from the light, shifts 3 px toward it and lifts up to 5 px. Measured in Chromium 151, Firefox 153 and WebKit 26.5 with the pointer at the right edge: 13.5 px of ground shadow slide. When the pointer leaves, everything returns to rest; the ground shadows follow with a 0.3 s transition.
The offset is computed once, from the container's center, and applies to all three badges: they react as under a distant sun, with parallel shadows, not as under a lamp placed between them, which would give each object its own shadow direction. The script listens to pointer events: with a finger, touch-action: pan-y keeps vertical page scrolling, and a sideways swipe moves the light; everything returns to rest when the finger lifts.
prefers-reduced-motion: reduce, a block scoped to .fx-0029-root forces transform: none on the badges: they no longer lift or shift. The shadows keep following the light, which keeps the "where the light comes from" cue without moving the objects. Measured in Chromium, Firefox and WebKit: badges still under the mouse, ground shadows still offset.aria-hidden="true" on the badges if they are decorative, as in the scenes.#6366f1 at 4.5:1 and on pink #ec4899 at 3.5:1; on the green #10b981, only 2.5:1: decorative, not for text.CSS (transform, box-shadow, radial gradients, transition, touch-action) + JS with pointer events, no dependencies. Measured in Chromium 151, Firefox 153 and WebKit 26.5: same offsets, two independent containers on one page, still badges under reduced motion; finger swipe measured in Chromium in touch mode. No CSS 3D: the WebKit rendering is reliable, a real Safari and a real iPhone were not checked.
Without JavaScript, the three badges stay on their contact shadows, at rest. On a touch screen, a single tap does not move the light: the finger has to slide sideways.
Copy the three blocks into your page. No dependencies.
<div class="fx-0029-root">
<div class="float-shadow-container" data-float-shadow>
<div class="float-shadow-el">
<div class="float-shadow-shape fs-shape-1">♦</div>
<div class="float-shadow-shadow"></div>
</div>
<div class="float-shadow-el">
<div class="float-shadow-shape fs-shape-2">★</div>
<div class="float-shadow-shadow"></div>
</div>
<div class="float-shadow-el">
<div class="float-shadow-shape fs-shape-3">♥</div>
<div class="float-shadow-shadow"></div>
</div>
</div>
</div>
.fx-0029-root,
.fx-0029-root *,
.fx-0029-root *::before,
.fx-0029-root *::after {
box-sizing: border-box
}
.fx-0029-root {
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;
}
.float-shadow-container {
width: 100%;
height: 250px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
gap: 30px;
cursor: crosshair;
/* Touch: a sideways drag moves the light, a vertical swipe still scrolls the page */
touch-action: pan-y;
}
.float-shadow-el {
position: relative;
transition: transform 0.3s ease;
}
.float-shadow-shape {
width: 60px;
height: 60px;
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
position: relative;
z-index: 2;
}
.fs-shape-1 {
background: linear-gradient(135deg, #6366f1, #818cf8);
}
.fs-shape-2 {
background: linear-gradient(135deg, #ec4899, #f472b6);
border-radius: 50%;
}
.fs-shape-3 {
background: linear-gradient(135deg, #10b981, #34d399);
}
.float-shadow-shadow {
position: absolute;
bottom: -15px;
left: 50%;
transform: translateX(-50%);
width: 50px;
height: 10px;
background: radial-gradient(ellipse, rgba(0, 0, 0, 0.35) 0%, transparent 70%);
border-radius: 50%;
transition: all 0.3s ease;
z-index: 1;
}
/* Reduced motion: the shapes no longer lift; only the shadows follow the light */
@media (prefers-reduced-motion: reduce) {
.fx-0029-root .float-shadow-shape {
transform: none !important;
}
}
(function() {
// ==========================================
// 9. Floating Shadows - Dynamic light source (pointer events: mouse, pen and touch)
// One instance per .fx-0029-root: several groups on one page each follow their own light.
// ==========================================
document.querySelectorAll('.fx-0029-root').forEach(function(root) {
var container = root.querySelector('[data-float-shadow]');
if (!container) return;
var elements = container.querySelectorAll('.float-shadow-el');
container.addEventListener('pointermove', function(e) {
var rect = container.getBoundingClientRect();
var mx = e.clientX - rect.left;
var my = e.clientY - rect.top;
var centerX = rect.width / 2;
var centerY = rect.height / 2;
var lightX = (mx - centerX) / centerX;
var lightY = (my - centerY) / centerY;
elements.forEach(function(el) {
var shape = el.querySelector('.float-shadow-shape');
var shadow = el.querySelector('.float-shadow-shadow');
// Move shadow opposite to light direction
var shadowX = -lightX * 15;
var shadowY = lightY * 5 + 5;
var shadowScale = 1 + Math.abs(lightY) * 0.3;
shadow.style.transform = 'translateX(calc(-50% + ' + shadowX + 'px)) scaleX(' + shadowScale + ')';
shadow.style.opacity = 0.6 - Math.abs(lightY) * 0.2;
// Lift shape slightly toward light
shape.style.transform = 'translate(' + (lightX * 3) + 'px, ' + (-Math.abs(lightY) * 5) + 'px)';
// Dynamic box shadow on shape
shape.style.boxShadow = ((-lightX * 8)) + 'px ' + ((-lightY * 8 + 4)) + 'px 20px rgba(0,0,0,0.3)';
});
});
container.addEventListener('pointerleave', function() {
elements.forEach(function(el) {
var shape = el.querySelector('.float-shadow-shape');
var shadow = el.querySelector('.float-shadow-shadow');
shadow.style.transform = 'translateX(-50%)';
shadow.style.opacity = '0.35';
shape.style.transform = 'translate(0, 0)';
shape.style.boxShadow = '';
});
});
});
})();
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
Ground shadow slide (JS — -lightX * 15) |
15 px at most | 30 gives long late-afternoon shadows; 8, a light almost overhead. |
Shadow stretch (JS — Math.abs(lightY) * 0.3) |
up to ×1.3 | Lengthens the shadow when the light is low or high. 0.6 stretches it clearly; 0 keeps it round. |
Badge drop shadow (JS — boxShadow, factor 8 and 20px blur) |
8 px offset, 20 px blur, black at 30% | A factor of 14 clearly lifts the badges off the background; a 10 px blur gives a harder light. |
Lift (JS — lightX * 3 and Math.abs(lightY) * 5) |
3 px sideways, 5 px up | Set both to 0 for objects that sit still and never move: only the shadows react. |
Badges (CSS — .float-shadow-shape, .fs-shape-1 to -3) |
60 × 60 px, indigo, pink, green gradients | Size, color and shape are free. Avoid clip-path on the badge: it would cut its drop shadow; draw the shape with border-radius or a ::before, like the roof in the second scene. |
Ground shadows (CSS — .float-shadow-shadow) |
50 × 10 px, black at 35%, 15 px under the badge | Widen them along with the badges (74 × 16 px for 78 px badges) and tint them to match the ground (dark green on a lawn). |
Not in this code: the light is computed once, from the container's center, and the same offset applies to all three, as under a distant sun. For a nearby lamp, you would compute lightX and lightY inside the loop, from the center of each element (el.getBoundingClientRect()), instead of the container's center.
The turning shadow is a box-shadow on the badge; a clip-path or a mask cuts it along with the shape. Draw the shape with border-radius, borders or a pseudo-element, or replace the box-shadow with filter: drop-shadow(…) on a parent element.
Yes. Each group has its own .fx-0029-root and the script gives each one its own instance: each group only reacts to the pointer hovering it. That is the case on this page, where the demo at the top and the three examples run on a single script.