02떠오름
호버에 6px 뜨고 그림자가 자란다.
쓰임카드형 버튼, 상품 담기
6px높게 10px그림자 없음

<main class="stage"><button class="btn is-demo" type="button">장바구니 담기</button></main>// ── 변수 3개 ──────────────────────────────────────────
$lift: 6px; // 얼마나 뜨나
$duration: 220ms;
$shadow: rgba(23, 20, 26, .32); // 그림자 색. 없애려면 transparent
html, body { margin: 0; overflow: hidden; } // iframe 안에서 스크롤바가 생기지 않게 (실측 2026-09-09: body 여백 8px 이 100vh 를 넘겼다)
.stage { min-height: 100vh; display: grid; place-items: center; margin: 0; background: #d2c7a4; font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif; }
.btn {
border: 0;
border-radius: 14px;
padding: 22px 56px;
font-weight: 800;
font-size: 20px;
line-height: 1;
font-family: inherit;
color: #fff7e6;
background: #17141a;
cursor: pointer;
will-change: transform;
transition: transform $duration cubic-bezier(.2, .8, .2, 1), box-shadow $duration;
&:hover, &:focus-visible { transform: translateY(-$lift); box-shadow: 0 14px 24px $shadow; }
&:active { transform: translateY(-2px); box-shadow: 0 6px 12px $shadow; }
// 데모 · 문서용 자동 반복
&.is-demo { animation: lift-loop 2s ease-in-out infinite; }
}
@keyframes lift-loop {
0%, 15% { transform: translateY(0); box-shadow: 0 0 0 rgba(23, 20, 26, 0); }
45%, 70% { transform: translateY(-$lift); box-shadow: 0 14px 24px $shadow; }
100% { transform: translateY(0); box-shadow: 0 0 0 rgba(23, 20, 26, 0); }
}
@media (prefers-reduced-motion: reduce) {
.btn, .btn.is-demo { transition: none; animation: none; }
.btn:hover { box-shadow: 0 14px 24px $shadow; } // 뜨지 않고 그림자만
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #d2c7a4;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.btn {
border: 0;
border-radius: 14px;
padding: 22px 56px;
font-weight: 800;
font-size: 20px;
line-height: 1;
font-family: inherit;
color: #fff7e6;
background: #17141a;
cursor: pointer;
will-change: transform;
transition: transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1), box-shadow 220ms;
}
.btn:hover, .btn:focus-visible {
transform: translateY(-6px);
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
}
.btn:active {
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(23, 20, 26, 0.32);
}
.btn.is-demo {
animation: lift-loop 2s ease-in-out infinite;
}
@keyframes lift-loop {
0%, 15% {
transform: translateY(0);
box-shadow: 0 0 0 rgba(23, 20, 26, 0);
}
45%, 70% {
transform: translateY(-6px);
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
}
100% {
transform: translateY(0);
box-shadow: 0 0 0 rgba(23, 20, 26, 0);
}
}
@media (prefers-reduced-motion: reduce) {
.btn, .btn.is-demo {
transition: none;
animation: none;
}
.btn:hover {
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
}
}// 데모(.is-demo)는 자동 반복. 사람이 올리면 진짜 호버로 넘어간다.
document.querySelectorAll('.btn').forEach(function (b) {
b.addEventListener('pointerenter', function () { b.classList.remove('is-demo'); });
});import type { CSSProperties, ReactNode } from "react";
import styles from "./LiftButton.module.scss";
export type LiftButtonProps = { children: ReactNode; lift?: number; duration?: number; shadow?: string; onClick?: () => void };
export default function LiftButton({ children, lift = 6, duration = 220, shadow = "rgba(23,20,26,.32)", onClick }: LiftButtonProps) {
const vars = { "--lift": `${lift}px`, "--duration": `${duration}ms`, "--shadow": shadow } as CSSProperties;
return (
<button type="button" className={styles.btn} style={vars} onClick={onClick}>
{children}
</button>
);
}02 떠오름 — 측정 누적면적 6.583% · 움직인 프레임 13/23GODRICH 원작 · zip 에 SCSS·React 포함 · CSS 버튼 애니메이션 9종 — 복붙하면 되는 SCSS + React
이 데모가 속한 글
CSS 버튼 애니메이션 9종 — 복붙하면 되는 SCSS + ReactCSS 버튼 애니메이션 아홉 개를 한 번에 모았습니다. 호버, 클릭, 로딩, 실패, 확인까지 버튼 하나가 겪는 상황을 아홉 장면으로 나눠 하나씩 담았고, 전부 transform과 opacity만 움직입니다. 격자에서 이 글의 데모 전부 보기


