01White-Border Die-Cut Sticker Card
Wraps an image or icon in a thick white border with a soft shadow behind it, so it reads like a real printed sticker peeled and stuck onto the page.
Best forPortfolio image decoration, feed-style cards
box-shadowclip-pathpointerenter

<main class="stage">
<div class="wrap is-demo" id="wrap">
<button class="badge" id="badge" type="button" aria-label="Die-cut sticker; hover or focus to lift it">
<span class="badgeMark" aria-hidden="true"></span>
</button>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 계산 겹은 index.html 의 shadowFor(t) 에 있다 — 들어올림 진행도(t, 0~1)에 비례해 그림자 레이어
// 5장의 오프셋·흐림·불투명도를 산술로 늘려 쌓아 하나의 box-shadow 로 합친다. 여기는 다이컷 모양의
// 흰 테두리와 배지 표면만 맡는다. transform·box-shadow 는 JS 가 매 프레임 직접 쓴다(rAF, paint 만).
@include stage($stage-ink);
.wrap {
display: grid;
place-items: center;
}
.badge {
position: relative;
width: 130px;
height: 130px;
border-radius: 50%;
border: 10px solid $subject-cream;
background: $subject-blue-deep;
padding: 0;
appearance: none;
cursor: pointer;
will-change: transform, box-shadow;
&:focus-visible { outline: 2px solid $subject-blue-light; outline-offset: 4px; }
}
.badgeMark {
position: absolute;
inset: 0;
margin: auto;
width: 46px;
height: 46px;
background: $subject-cream;
clip-path: polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%);
}
@media (max-width: 340px) {
.badge { width: 100px; height: 100px; border-width: 8px; }
.badgeMark { width: 36px; height: 36px; }
}
@media (prefers-reduced-motion: reduce) {
// 실사용 경로(pointerenter/focus)는 그대로 두되, 자동 루프의 시각 전환만 즉시 스냅되도록
// index.html 의 reduce 분기가 lerp 를 건너뛴다 — 여긴 transition 이 없어 추가로 끌 것이 없다.
.badge { transition: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.wrap {
display: grid;
place-items: center;
}
.badge {
position: relative;
width: 130px;
height: 130px;
border-radius: 50%;
border: 10px solid #fff7e6;
background: #0a4fe8;
padding: 0;
appearance: none;
cursor: pointer;
will-change: transform, box-shadow;
}
.badge:focus-visible {
outline: 2px solid #5b8cf8;
outline-offset: 4px;
}
.badgeMark {
position: absolute;
inset: 0;
margin: auto;
width: 46px;
height: 46px;
background: #fff7e6;
clip-path: polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%);
}
@media (max-width: 340px) {
.badge {
width: 100px;
height: 100px;
border-width: 8px;
}
.badgeMark {
width: 36px;
height: 36px;
}
}
@media (prefers-reduced-motion: reduce) {
.badge {
transition: none;
}
}(function () {
var LIFT_LAYERS = 5; // 그림자를 쌓는 겹 수
var LAYER_OFFSET = 3.6; // 겹마다 늘어나는 세로 오프셋(px, 완전히 뜬 상태 기준)
var LAYER_BLUR = 6; // 겹마다 늘어나는 흐림(px)
var LAYER_ALPHA_BASE = .34; // 첫 겹 불투명도
var LAYER_ALPHA_STEP = .05; // 겹마다 옅어지는 정도
var LIFT_PX = 18; // 완전히 뜬 상태에서 위로 이동하는 거리(px)
var LIFT_SCALE = .08; // 완전히 뜬 상태에서 커지는 비율
var LERP_RATE = .16;
var STEP_MS = 900;
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var wrap = document.getElementById('wrap');
var badge = document.getElementById('badge');
var isDemo = true;
var target = 0; // 0 = 붙어 있음, 1 = 완전히 뜸
var shown = 0;
// 겹마다 오프셋·흐림·불투명도를 산술로 늘려 쌓는다 — 하나의 box-shadow 문자열로 합쳐 다이컷 스티커의
// 두꺼운 그림자 뒤판을 만든다. 이 계산이 뜬 정도(t)에 정비례해 매 프레임 다시 도는 것이 계산 겹이다.
function shadowFor(t) {
var parts = [];
for (var i = 1; i <= LIFT_LAYERS; i++) {
var off = (LAYER_OFFSET * i * t).toFixed(2);
var blur = (LAYER_BLUR * i * t + 2).toFixed(2);
var alpha = Math.max(0, LAYER_ALPHA_BASE - LAYER_ALPHA_STEP * i) * t;
parts.push('0 ' + off + 'px ' + blur + 'px rgba(10,8,14,' + alpha.toFixed(3) + ')');
}
return parts.join(', ');
}
function human() { if (isDemo) { wrap.classList.remove('is-demo'); isDemo = false; } }
function lift() { human(); target = 1; }
function drop() { human(); target = 0; }
badge.addEventListener('pointerenter', lift);
badge.addEventListener('pointerleave', drop);
badge.addEventListener('focus', lift);
badge.addEventListener('blur', drop);
var dir = 1;
var last = 0;
function autoAdvance(ts) {
if (ts - last < STEP_MS) return;
last = ts;
target = dir > 0 ? 1 : 0;
dir *= -1;
}
function loop(ts) {
if (!last) last = ts;
if (isDemo && !reduce) autoAdvance(ts);
if (reduce) { shown = target; } else { shown += (target - shown) * LERP_RATE; }
badge.style.transform = 'translateY(' + (-LIFT_PX * shown).toFixed(2) + 'px) scale(' + (1 + LIFT_SCALE * shown).toFixed(3) + ')';
badge.style.boxShadow = shadowFor(shown);
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
})();// WhiteBorderDropShadow — vanilla 판(run/378/demo/01-sticker-white-border-drop-shadow/index.html) 의 React 포트.
// 들어올림 진행도(t)에 비례해 그림자 레이어 5장의 오프셋·흐림·불투명도를 산술로 늘려 쌓아
// 하나의 box-shadow 로 합친다 — 계산 겹은 shadowFor(t) 에 있다.
import { useEffect, useRef } from "react";
import styles from "./WhiteBorderDropShadow.module.scss";
type Box<T> = { current: T };
const LIFT_LAYERS = 5;
const LAYER_OFFSET = 3.6;
const LAYER_BLUR = 6;
const LAYER_ALPHA_BASE = 0.34;
const LAYER_ALPHA_STEP = 0.05;
const LIFT_PX = 18;
const LIFT_SCALE = 0.08;
const LERP_RATE = 0.16;
const STEP_MS = 900;
export default function WhiteBorderDropShadow() {
const rootRef = useRef(null) as unknown as Box<HTMLDivElement>;
const badgeRef = useRef(null) as unknown as Box<HTMLButtonElement>;
const rafRef = useRef(0) as unknown as Box<number>;
useEffect(() => {
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const wrap = rootRef.current;
const badge = badgeRef.current;
if (!wrap || !badge) return;
let isDemo = true;
let target = 0;
let shown = 0;
function shadowFor(t: number) {
const parts: string[] = [];
for (let i = 1; i <= LIFT_LAYERS; i++) {
const off = (LAYER_OFFSET * i * t).toFixed(2);
const blur = (LAYER_BLUR * i * t + 2).toFixed(2);
const alpha = Math.max(0, LAYER_ALPHA_BASE - LAYER_ALPHA_STEP * i) * t;
parts.push("0 " + off + "px " + blur + "px rgba(10,8,14," + alpha.toFixed(3) + ")");
}
return parts.join(", ");
}
function human() { if (isDemo) { wrap!.classList.remove(styles.isDemo); isDemo = false; } }
function lift() { human(); target = 1; }
function drop() { human(); target = 0; }
badge.addEventListener("pointerenter", lift);
badge.addEventListener("pointerleave", drop);
badge.addEventListener("focus", lift);
badge.addEventListener("blur", drop);
let dir = 1;
let last = 0;
function autoAdvance(ts: number) {
if (ts - last < STEP_MS) return;
last = ts;
target = dir > 0 ? 1 : 0;
dir *= -1;
}
function loop(ts: number) {
if (!last) last = ts;
if (isDemo && !reduce) autoAdvance(ts);
if (reduce) { shown = target; } else { shown += (target - shown) * LERP_RATE; }
badge!.style.transform = "translateY(" + (-LIFT_PX * shown).toFixed(2) + "px) scale(" + (1 + LIFT_SCALE * shown).toFixed(3) + ")";
badge!.style.boxShadow = shadowFor(shown);
rafRef.current = requestAnimationFrame(loop);
}
rafRef.current = requestAnimationFrame(loop);
return () => {
badge!.removeEventListener("pointerenter", lift);
badge!.removeEventListener("pointerleave", drop);
badge!.removeEventListener("focus", lift);
badge!.removeEventListener("blur", drop);
cancelAnimationFrame(rafRef.current);
};
}, []);
return (
<main className={styles.stage}>
<div className={styles.wrap + " " + styles.isDemo} ref={rootRef}>
<button className={styles.badge} ref={badgeRef} type="button" aria-label="다이컷 스티커, 마우스를 올리거나 초점을 주면 들어올려진다">
<span className={styles.badgeMark} aria-hidden="true"></span>
</button>
</div>
</main>
);
}01 White-Border Die-Cut Sticker Card — Measured area 7.179% · moving frames 15/23Original by GODRICH · SCSS and React ship in the zip · CSS Sticker Peel Effect: 9 Styles, Border to Holo
The post this demo lives in
CSS Sticker Peel Effect: 9 Styles, Border to HoloAmong nine css sticker peel effect ideas, one fakes a die-cut look with a white border and shadow, one sweeps rainbow li See all demos in this post


