01롱섀도 레트로 — 뻗어나가는 그림자의 길이 값이 호버 상태에 따라 늘고 줄어드는 버튼
그림자가 뻗어나가는 길이 값이 호버 상태가 되면 정해진 목표 값까지 늘어나고, 호버가 풀린 상태가 되면 다시 원래 길이 값으로 돌아옵니다.
쓰임랜딩 페이지 CTA, 프로모션 배너 버튼
MAXLEN=22buildShadowlerp

<main class="stage">
<div class="ctalsr is-demo" id="ctalsr">
<div class="ctalsr__panel">
<button class="ctalsr__btn" id="ctalsrBtn" type="button">지금 시작하기</button>
<span class="ctalsr__hint">호버하면 그림자 길이가 늘어납니다</span>
</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 계산 겹(색·거리 보간)은 JS 에 있다 — 여기는 정적 배치만.
@include stage($stage-yellow);
.stage { position: relative; }
.ctalsr__panel {
display: flex;
flex-direction: column;
align-items: center;
gap: $sp-4;
padding: $sp-5;
border-radius: $r-card;
background: $stage-paper;
}
.ctalsr__btn {
border: 0;
border-radius: $r-control;
background: $subject-blue-deep;
color: $subject-cream;
font-family: inherit;
font-weight: 700;
font-size: 16px;
padding: $sp-4 $sp-5;
cursor: pointer;
}
.ctalsr__hint {
color: rgba($subject-ink, .72);
font-size: 12px;
text-align: center;
}
@media (max-width: 340px) {
.ctalsr__panel { padding: $sp-4; gap: $sp-3; }
.ctalsr__btn { padding: $sp-3 $sp-4; font-size: 14px; }
}
@media (prefers-reduced-motion: reduce) {
.ctalsr__btn { transition: none !important; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffd23f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.stage {
position: relative;
}
.ctalsr__panel {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
padding: 20px;
border-radius: 16px;
background: #ffffff;
}
.ctalsr__btn {
border: 0;
border-radius: 12px;
background: #0a4fe8;
color: #fff7e6;
font-family: inherit;
font-weight: 700;
font-size: 16px;
padding: 16px 20px;
cursor: pointer;
}
.ctalsr__hint {
color: rgba(23, 20, 26, 0.72);
font-size: 12px;
text-align: center;
}
@media (max-width: 340px) {
.ctalsr__panel {
padding: 16px;
gap: 12px;
}
.ctalsr__btn {
padding: 12px 16px;
font-size: 14px;
}
}
@media (prefers-reduced-motion: reduce) {
.ctalsr__btn {
transition: none !important;
}
}(function () {
var root = document.getElementById('ctalsr');
var btn = document.getElementById('ctalsrBtn');
// 계산 겹: 그림자 길이 값을 매 프레임 목표로 선형 보간(0.28)해 부드럽게 좇고,
// 그 길이만큼 45도 방향(dx=dy=1px)으로 레이어를 쌓아 box-shadow 문자열을 만든다.
// 레이어마다 색을 옅은 파랑에서 짙은 잉크색으로 선형 보간해 원근감을 준다.
var MAXLEN = 22;
var C_NEAR = [47, 109, 246]; // $subject-blue
var C_FAR = [23, 20, 26]; // $subject-ink
var len = 0, target = 0, raf = null;
function lerp(a, b, t) { return a + (b - a) * t; }
function buildShadow(l) {
var n = Math.round(l);
if (n <= 0) { return 'none'; }
var parts = [];
for (var i = 1; i <= n; i++) {
var t = i / n;
var r = Math.round(lerp(C_NEAR[0], C_FAR[0], t));
var g = Math.round(lerp(C_NEAR[1], C_FAR[1], t));
var b = Math.round(lerp(C_NEAR[2], C_FAR[2], t));
parts.push(i + 'px ' + i + 'px 0 rgb(' + r + ',' + g + ',' + b + ')');
}
return parts.join(',');
}
function step() {
var diff = target - len;
len += diff * 0.28;
if (Math.abs(diff) < 0.05) { len = target; }
btn.style.boxShadow = buildShadow(len);
if (Math.abs(target - len) > 0.05) {
raf = requestAnimationFrame(step);
} else {
raf = null;
}
}
function go(to) {
target = to;
if (raf === null) { raf = requestAnimationFrame(step); }
}
function wireReal() {
btn.addEventListener('mouseenter', function () { root.classList.remove('is-demo'); stopAutoplay(); go(MAXLEN); });
btn.addEventListener('mouseleave', function () { go(0); });
btn.addEventListener('focus', function () { root.classList.remove('is-demo'); stopAutoplay(); go(MAXLEN); });
btn.addEventListener('blur', function () { go(0); });
}
wireReal();
var hovered = false;
var autoplayTimer = null;
function autoplayTick() { hovered = !hovered; go(hovered ? MAXLEN : 0); }
function stopAutoplay() { if (autoplayTimer) { clearInterval(autoplayTimer); autoplayTimer = null; } }
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (!reduce) { autoplayTimer = setInterval(autoplayTick, 700); }
btn.style.boxShadow = 'none';
})();import { useEffect, useRef } from "react";
import styles from "./CtaBtnLongShadowRetro.module.scss";
export type CtaBtnLongShadowRetroProps = {
color?: string;
};
export default function CtaBtnLongShadowRetro({
color = "#0a4fe8",
}: CtaBtnLongShadowRetroProps) {
const rootRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const root = rootRef.current;
if (!root) return;
const btn = root.querySelector<HTMLButtonElement>(`.${styles.btn}`);
if (!btn) return;
// 계산 겹: 그림자 길이 값을 매 프레임 목표로 선형 보간(0.28)해 부드럽게 좇고,
// 그 길이만큼 45도 방향(dx=dy=1px)으로 레이어를 쌓아 box-shadow 문자열을 만든다.
const MAXLEN = 22;
const C_NEAR: [number, number, number] = [47, 109, 246];
const C_FAR: [number, number, number] = [23, 20, 26];
let len = 0;
let target = 0;
let raf: number | null = null;
function lerp(a: number, b: number, t: number) { return a + (b - a) * t; }
function buildShadow(l: number) {
const n = Math.round(l);
if (n <= 0) return "none";
const parts: string[] = [];
for (let i = 1; i <= n; i++) {
const t = i / n;
const r = Math.round(lerp(C_NEAR[0], C_FAR[0], t));
const g = Math.round(lerp(C_NEAR[1], C_FAR[1], t));
const b = Math.round(lerp(C_NEAR[2], C_FAR[2], t));
parts.push(i + "px " + i + "px 0 rgb(" + r + "," + g + "," + b + ")");
}
return parts.join(",");
}
function step() {
const diff = target - len;
len += diff * 0.28;
if (Math.abs(diff) < 0.05) { len = target; }
btn!.style.boxShadow = buildShadow(len);
if (Math.abs(target - len) > 0.05) {
raf = requestAnimationFrame(step);
} else {
raf = null;
}
}
function go(to: number) {
target = to;
if (raf === null) { raf = requestAnimationFrame(step); }
}
function onEnter() { stopAutoplay(); go(MAXLEN); }
function onLeave() { go(0); }
btn.addEventListener("mouseenter", onEnter);
btn.addEventListener("mouseleave", onLeave);
btn.addEventListener("focus", onEnter);
btn.addEventListener("blur", onLeave);
let hovered = false;
let autoplayTimer: number | null = null;
function autoplayTick() { hovered = !hovered; go(hovered ? MAXLEN : 0); }
function stopAutoplay() { if (autoplayTimer !== null) { window.clearInterval(autoplayTimer); autoplayTimer = null; } }
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (!reduce) { autoplayTimer = window.setInterval(autoplayTick, 700); }
btn.style.boxShadow = "none";
return () => {
stopAutoplay();
if (raf !== null) { cancelAnimationFrame(raf); }
btn.removeEventListener("mouseenter", onEnter);
btn.removeEventListener("mouseleave", onLeave);
btn.removeEventListener("focus", onEnter);
btn.removeEventListener("blur", onLeave);
};
}, []);
return (
<div ref={rootRef} className={styles.stage}>
<div className={styles.panel}>
<button className={styles.btn} type="button" style={{ background: color }}>
지금 시작하기
</button>
<span className={styles.hint}>호버하면 그림자 길이가 늘어납니다</span>
</div>
</div>
);
}01 롱섀도 레트로 — 뻗어나가는 그림자의 길이 값이 호버 상태에 따라 늘고 줄어드는 버튼 — 측정 누적면적 2.695% · 움직인 프레임 8/23GODRICH 원작 · zip 에 SCSS·React 포함 · CTA 버튼 디자인 9종 — 그림자가 늘어나는 것부터 막대가 튀는 것까지
이 데모가 속한 글
CTA 버튼 디자인 9종 — 그림자가 늘어나는 것부터 막대가 튀는 것까지cta 버튼 디자인은 클릭을 부르는 버튼에 상태 변화를 계산해 얹는 작업인데, 아홉 개 중 하나는 그림자가 뻗어나가는 길이 값이 호버 상태에 따라 늘고 줄고, 하나는 둘레를 감싼 진행 값이 정해진 시간 동안 0까지 이 글의 데모 전부 보기


