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 オリジナル · SCSS と React は zip に同梱 · CTA ボタン デザイン 9選 — 伸びる影から跳ねるバーまで
このデモが属する記事
CTA ボタン デザイン 9選 — 伸びる影から跳ねるバーまで9つのうち1つは伸びる影の長さの値がホバー状態に応じて伸び縮みし、1つは周囲の進行の値が決まった時間の間に0まで減り、もう1つは引いた位置の値が基準を超えると確定した状態に変わります。cta ボタン デザインの9つの動きを見ていきます。 この記事のデモをすべて見る


