03밑줄 슬라이드
밑줄이 왼쪽에서 오른쪽으로 그어지고, 떠나면 오른쪽으로 빠진다.
쓰임텍스트형 링크 버튼, 내비게이션
2px굵게 3px가운데서

<main class="stage"><a class="link is-demo" href="#">더 알아보기</a></main>// ── 변수 3개 ──────────────────────────────────────────
$thickness: 3px;
$duration: 260ms;
$color: #fff7e6;
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: #c89a8f; font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif; }
.link {
position: relative;
display: inline-block;
padding: 6px 2px;
font-weight: 800;
font-size: 30px;
line-height: 1.2;
font-family: inherit;
color: $color;
text-decoration: none;
&::after {
content: "";
position: absolute;
left: 0; right: 0; bottom: 0;
height: $thickness;
background: currentColor;
transform: scaleX(0);
transform-origin: right; // 떠날 때는 오른쪽으로 빠진다
transition: transform $duration cubic-bezier(.2, .8, .2, 1);
}
&:hover::after, &:focus-visible::after {
transform: scaleX(1);
transform-origin: left; // 들어올 때는 왼쪽에서 그어진다
}
&.is-demo::after { animation: underline-loop 2s cubic-bezier(.2, .8, .2, 1) infinite; }
}
@keyframes underline-loop {
0% { transform: scaleX(0); transform-origin: left; } // 규칙8: 언더라인 왼쪽 진입
35%, 55% { transform: scaleX(1); transform-origin: left; } // 규칙8: 언더라인 유지 좌기준
56% { transform: scaleX(1); transform-origin: right; } // 규칙8: 기준 오른쪽으로 전환
90%, 100% { transform: scaleX(0); transform-origin: right; } // 규칙8: 언더라인 오른쪽 소멸
}
@media (prefers-reduced-motion: reduce) {
.link::after, .link.is-demo::after { transition: none; animation: none; }
.link:hover::after { transform: scaleX(1); }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #c89a8f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.link {
position: relative;
display: inline-block;
padding: 6px 2px;
font-weight: 800;
font-size: 30px;
line-height: 1.2;
font-family: inherit;
color: #fff7e6;
text-decoration: none;
}
.link::after {
content: "";
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 3px;
background: currentColor;
transform: scaleX(0);
transform-origin: right;
transition: transform 260ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.link:hover::after, .link:focus-visible::after {
transform: scaleX(1);
transform-origin: left;
}
.link.is-demo::after {
animation: underline-loop 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
@keyframes underline-loop {
0% {
transform: scaleX(0);
transform-origin: left;
}
35%, 55% {
transform: scaleX(1);
transform-origin: left;
}
56% {
transform: scaleX(1);
transform-origin: right;
}
90%, 100% {
transform: scaleX(0);
transform-origin: right;
}
}
@media (prefers-reduced-motion: reduce) {
.link::after, .link.is-demo::after {
transition: none;
animation: none;
}
.link:hover::after {
transform: scaleX(1);
}
}document.querySelectorAll('.link').forEach(function (b) {
b.addEventListener('pointerenter', function () { b.classList.remove('is-demo'); });
b.addEventListener('click', function (e) { e.preventDefault(); });
});import type { CSSProperties, ReactNode } from "react";
import styles from "./UnderlineLink.module.scss";
export type UnderlineLinkProps = { children: ReactNode; href: string; thickness?: number; duration?: number; color?: string };
export default function UnderlineLink({ children, href, thickness = 3, duration = 260, color = "currentColor" }: UnderlineLinkProps) {
const vars = { "--thickness": `${thickness}px`, "--duration": `${duration}ms`, "--color": color } as CSSProperties;
return (
<a className={styles.link} href={href} style={vars}>
{children}
</a>
);
}03 밑줄 슬라이드 — 측정 누적면적 0.339% · 움직인 프레임 16/23GODRICH 원작 · zip 에 SCSS·React 포함 · CSS 버튼 애니메이션 9종 — 복붙하면 되는 SCSS + React
이 데모가 속한 글
CSS 버튼 애니메이션 9종 — 복붙하면 되는 SCSS + ReactCSS 버튼 애니메이션 아홉 개를 한 번에 모았습니다. 호버, 클릭, 로딩, 실패, 확인까지 버튼 하나가 겪는 상황을 아홉 장면으로 나눠 하나씩 담았고, 전부 transform과 opacity만 움직입니다. 격자에서 이 글의 데모 전부 보기

