07Floating Cards Whose Shadow Grows With Their Height
Each card's float height is converted into the shadow's blur radius and opacity, so a card floating higher casts a larger, fainter shadow while a low one casts a tight, dark one.
Best forFeature section where cards appear to float
shadowForHeightcardBobAtCARD_FLOAT_AMPLITUDE_PX

<main class="stage">
<div class="d3 is-demo" id="d3Root" tabindex="0">
<canvas class="d3__canvas" id="d3Canvas" width="480" height="300"></canvas>
<p class="d3__caption">Each card's float height is converted into the shadow's blur radius and opacity, so a card floating higher casts a larger, fainter shadow while a low one casts a tight, dark one</p>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
@include stage($stage-orange);
.d3 {
position: relative;
width: min(92vw, calc(160vh - 12.8vw));
aspect-ratio: 480 / 300;
margin: 0 auto;
border-radius: $r-card;
overflow: hidden;
box-shadow: $shadow-raised;
background: $stage-orange;
outline: none;
}
.d3__canvas {
display: block;
width: 100%;
height: 100%;
}
.d3__caption {
position: absolute;
left: $sp-3;
bottom: $sp-3;
max-width: 78%;
margin: 0;
padding: $sp-1 $sp-3;
border-radius: $r-pill;
background: rgba(23, 20, 26, .78);
color: #fff;
font-weight: 500;
font-size: 12px;
line-height: 1.4;
pointer-events: none;
}
@media (max-width: 340px) {
.d3__caption { font-size: 10px; padding: $sp-1 $sp-2; }
}
@media (prefers-reduced-motion: reduce) {
.d3 { cursor: default; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ff4d1f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.d3 {
position: relative;
width: min(92vw, 160vh - 12.8vw);
aspect-ratio: 480/300;
margin: 0 auto;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
background: #ff4d1f;
outline: none;
}
.d3__canvas {
display: block;
width: 100%;
height: 100%;
}
.d3__caption {
position: absolute;
left: 12px;
bottom: 12px;
max-width: 78%;
margin: 0;
padding: 4px 12px;
border-radius: 999px;
background: rgba(23, 20, 26, 0.78);
color: #fff;
font-weight: 500;
font-size: 12px;
line-height: 1.4;
pointer-events: none;
}
@media (max-width: 340px) {
.d3__caption {
font-size: 10px;
padding: 4px 8px;
}
}
@media (prefers-reduced-motion: reduce) {
.d3 {
cursor: default;
}
}(function () {
// 기법: cardBobAt 이 카드마다 CARD_FLOAT_AMPLITUDE_PX 범위에서 뜬 높이값을 매 프레임 계산하고,
// shadowForHeight 가 그 높이값을 그림자의 흐림 반경·불투명도로 변환한다 — 높이 뜬 카드는 그림자
// 반경이 커지고 옅어지며, 낮게 뜬 카드는 반경이 작고 진해진다(속성 트윈이 아니라 매핑 계산).
var root = document.getElementById('d3Root');
var canvas = document.getElementById('d3Canvas');
var ctx = canvas.getContext('2d');
var wrap = canvas.parentElement;
var CALC_W = 160, CALC_H = 100;
var CARD_FLOAT_AMPLITUDE_PX = 22;
var CARD_COUNT = 6;
var BASELINE_Y = CALC_H * 0.72;
function seedRand(seed) {
var s = seed >>> 0;
return function () { s = (s * 1664525 + 1013904223) >>> 0; return s / 4294967296; };
}
var rnd = seedRand(429071);
var palette = ['#6ec8ff', '#b9a0ff', '#ff9ebb', '#4cd4a6', '#ffd23f', '#ffffff'];
var cards = [];
for (var i = 0; i < CARD_COUNT; i += 1) {
cards.push({
x: 18 + (CALC_W - 36) * (i / (CARD_COUNT - 1)),
w: 16, h: 22,
bobSpeed: 0.0011 + rnd() * 0.0009,
phase: rnd() * Math.PI * 2,
color: palette[i % palette.length]
});
}
function cardBobAt(card, t) {
return (Math.sin(t * card.bobSpeed + card.phase) * 0.5 + 0.5) * CARD_FLOAT_AMPLITUDE_PX;
}
function shadowForHeight(h) {
var norm = h / CARD_FLOAT_AMPLITUDE_PX;
return {
blur: 3 + norm * 13,
alpha: 0.4 - norm * 0.26,
scaleX: 1 + norm * 0.35
};
}
var elapsed = 0;
var cw = 480, ch = 300;
function drawScene(t) {
var sx = cw / CALC_W, sy = ch / CALC_H;
ctx.clearRect(0, 0, cw, ch);
for (var i = 0; i < cards.length; i += 1) {
var card = cards[i];
var h = cardBobAt(card, t);
var sh = shadowForHeight(h);
ctx.save();
ctx.filter = 'blur(' + (sh.blur * sx) + 'px)';
ctx.fillStyle = 'rgba(20,16,24,' + sh.alpha + ')';
ctx.beginPath();
ctx.ellipse(card.x * sx, BASELINE_Y * sy, card.w * sh.scaleX * 0.5 * sx, card.h * 0.16 * sy, 0, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
var cardY = BASELINE_Y - card.h * 0.5 - h;
ctx.fillStyle = card.color;
var rx = card.x * sx - (card.w * 0.5) * sx;
var ry = cardY * sy - (card.h * 0.5) * sy;
var rw = card.w * sx, rh = card.h * sy;
var rad = 4 * sx;
ctx.beginPath();
ctx.moveTo(rx + rad, ry);
ctx.arcTo(rx + rw, ry, rx + rw, ry + rh, rad);
ctx.arcTo(rx + rw, ry + rh, rx, ry + rh, rad);
ctx.arcTo(rx, ry + rh, rx, ry, rad);
ctx.arcTo(rx, ry, rx + rw, ry, rad);
ctx.closePath();
ctx.fill();
}
}
function fit() {
var r = wrap.getBoundingClientRect();
cw = Math.max(1, Math.round(r.width));
ch = Math.max(1, Math.round(r.height));
canvas.width = cw; canvas.height = ch;
drawScene(elapsed);
}
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (reduce) root.classList.remove('is-demo');
var warmSteps = reduce ? 0 : 90;
for (var w = 0; w < warmSteps; w += 1) elapsed += 20;
var lastTs = null;
function tick(ts) {
if (lastTs === null) lastTs = ts;
var dt = Math.min(ts - lastTs, 48);
lastTs = ts;
if (!reduce) {
elapsed += dt;
drawScene(elapsed);
}
requestAnimationFrame(tick);
}
fit();
window.addEventListener('resize', fit);
drawScene(elapsed);
requestAnimationFrame(tick);
})();import { useEffect, useRef } from "react";
import styles from "./Depth3dCardFloatShadowDepth.module.scss";
type Card = {
x: number;
w: number;
h: number;
bobSpeed: number;
phase: number;
color: string;
};
export default function Depth3dCardFloatShadowDepth() {
const rootRef = useRef<HTMLDivElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const root = rootRef.current;
const canvas = canvasRef.current;
if (!root || !canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const wrap = canvas.parentElement as HTMLElement;
const CALC_W = 160, CALC_H = 100;
const CARD_FLOAT_AMPLITUDE_PX = 22;
const CARD_COUNT = 6;
const BASELINE_Y = CALC_H * 0.72;
function seedRand(seed: number) {
let s = seed >>> 0;
return function () { s = (s * 1664525 + 1013904223) >>> 0; return s / 4294967296; };
}
const rnd = seedRand(429071);
const palette = ["#6ec8ff", "#b9a0ff", "#ff9ebb", "#4cd4a6", "#ffd23f", "#ffffff"];
const cards: Card[] = [];
for (let i = 0; i < CARD_COUNT; i += 1) {
cards.push({
x: 18 + (CALC_W - 36) * (i / (CARD_COUNT - 1)),
w: 16, h: 22,
bobSpeed: 0.0011 + rnd() * 0.0009,
phase: rnd() * Math.PI * 2,
color: palette[i % palette.length]
});
}
function cardBobAt(card: Card, t: number) {
return (Math.sin(t * card.bobSpeed + card.phase) * 0.5 + 0.5) * CARD_FLOAT_AMPLITUDE_PX;
}
function shadowForHeight(h: number) {
const norm = h / CARD_FLOAT_AMPLITUDE_PX;
return {
blur: 3 + norm * 13,
alpha: 0.4 - norm * 0.26,
scaleX: 1 + norm * 0.35
};
}
let elapsed = 0;
let cw = 480, ch = 300;
function drawScene(t: number) {
const sx = cw / CALC_W, sy = ch / CALC_H;
ctx!.clearRect(0, 0, cw, ch);
for (let i = 0; i < cards.length; i += 1) {
const card = cards[i];
const h = cardBobAt(card, t);
const sh = shadowForHeight(h);
ctx!.save();
ctx!.filter = `blur(${sh.blur * sx}px)`;
ctx!.fillStyle = `rgba(20,16,24,${sh.alpha})`;
ctx!.beginPath();
ctx!.ellipse(card.x * sx, BASELINE_Y * sy, card.w * sh.scaleX * 0.5 * sx, card.h * 0.16 * sy, 0, 0, Math.PI * 2);
ctx!.fill();
ctx!.restore();
const cardY = BASELINE_Y - card.h * 0.5 - h;
ctx!.fillStyle = card.color;
const rx = card.x * sx - (card.w * 0.5) * sx;
const ry = cardY * sy - (card.h * 0.5) * sy;
const rw = card.w * sx, rh = card.h * sy;
const rad = 4 * sx;
ctx!.beginPath();
ctx!.moveTo(rx + rad, ry);
ctx!.arcTo(rx + rw, ry, rx + rw, ry + rh, rad);
ctx!.arcTo(rx + rw, ry + rh, rx, ry + rh, rad);
ctx!.arcTo(rx, ry + rh, rx, ry, rad);
ctx!.arcTo(rx, ry, rx + rw, ry, rad);
ctx!.closePath();
ctx!.fill();
}
}
function fit() {
const r = wrap.getBoundingClientRect();
cw = Math.max(1, Math.round(r.width));
ch = Math.max(1, Math.round(r.height));
canvas!.width = cw; canvas!.height = ch;
drawScene(elapsed);
}
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (reduce) root.classList.remove("is-demo");
const warmSteps = reduce ? 0 : 90;
for (let w = 0; w < warmSteps; w += 1) elapsed += 20;
let lastTs: number | null = null;
let raf = 0;
function tick(ts: number) {
if (lastTs === null) lastTs = ts;
const dt = Math.min(ts - lastTs, 48);
lastTs = ts;
if (!reduce) {
elapsed += dt;
drawScene(elapsed);
}
raf = requestAnimationFrame(tick);
}
fit();
window.addEventListener("resize", fit);
drawScene(elapsed);
raf = requestAnimationFrame(tick);
return () => {
cancelAnimationFrame(raf);
window.removeEventListener("resize", fit);
};
}, []);
return (
<main className={styles.stage}>
<div className={`${styles.d3} is-demo`} ref={rootRef} tabIndex={0}>
<canvas className={styles.d3__canvas} ref={canvasRef} width={480} height={300} />
<p className={styles.d3__caption}>카드마다 떠 있는 높이값을 그림자의 흐림 반경과 옅기로 변환해, 높이 뜬 카드일수록 그림자가 크고 연해지고 낮게 뜬 카드는 그림자가 진하고 작다</p>
</div>
</main>
);
}07 Floating Cards Whose Shadow Grows With Their Height — Measured area 21.405% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · 9 CSS 3D Background Animation Depth Layers
The post this demo lives in
9 CSS 3D Background Animation Depth LayersThis css 3d background animation set has nine layers below. Layer 02 loses focus with distance, layer 07 loses shadow st See all demos in this post
If you like this effect - similar demos
Interactions06 Nested Popovers Whose Shadow and Size Step Up with Each DepthCSS Popover: 9 Ways to Anchor, Flip and Spring
Interactions04 Card Stack With Perspective DepthCard Swipe UI: 9 Physics-Based Deck Patterns
Interactions02 Card stack that shifts by depth3D Hover Effect CSS: 9 Pointer-Following Scenes