CSS 밑줄 애니메이션 9종 — 한 줄 복붙으로
css 밑줄 애니메이션은 마우스를 올리거나 키보드로 초점을 옮겼을 때만 링크 아래 줄이 움직이며 나타나는 효과로, 지금 가리키고 있는 링크가 어디인지 눈으로 바로 알려줍니다. 아홉 가지를 모았고 전부 한 줄 복붙으로 그대로 붙여 쓸 수 있습니다.
자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip
아홉 개는 인기 순이 아니라 링크를 마주치는 순서대로 놓았습니다. 가장 기본이 되는 좌우 밑줄(01)에서 시작해 같은 방식의 변형(02)을 보여주고, 실제로 메뉴 글자가 줄바꿈되는 상황(03)을 짚은 뒤, 더 강한 강조(04)와 브랜드 색(05)을 더합니다. 이어서 장난스러운 물결(06)로 톤을 풀고, 삭제·완료 같은 상태 변화를 암시하는 취소선(07), 추천 링크를 표시하는 형광펜(08), 마지막으로 아직 안 읽은 링크가 확정 상태로 바뀌는 점선→실선(09)까지 이어집니다.
왜 :hover 뿐 아니라 :focus-visible 도 같이 걸어야 하나요
마우스가 없는 키보드 사용자는 Tab 키로 링크 사이를 옮겨 다닙니다. 아홉 개 전부 :hover 옆에 :focus-visible 을 나란히 걸어, 키보드로 초점이 옮겨 왔을 때도 마우스를 올린 것과 똑같이 밑줄이 움직이게 했습니다. 갤러리에 렌더된 데모는 .is-demo 클래스가 같은 모양을 2초마다 강제로 반복해 보여줄 뿐이고, 실제 페이지에 붙이면 진짜 호버·포커스에서만 움직입니다.
01좌→우 밑줄
가장 기본이 되는 형태로, 링크 아래 얇은 막대가 왼쪽에서 시작해 오른쪽으로 자라며 그어집니다. 마우스를 떼면 같은 방향으로 다시 줄어들어, 들어올 때와 나갈 때가 대칭을 이룹니다.
.link {
position: relative;
&::after {
content: "";
position: absolute;
left: 0; right: 0; bottom: 0;
height: 3px;
background: currentColor;
transform: scaleX(0);
transform-origin: left center;
transition: transform 220ms $ease-spring;
}
&:hover::after, &:focus-visible::after {
transform: scaleX(1);
}
}
02중앙에서 확장
01과 같은 막대를 쓰되 기준점만 가운데로 옮겼습니다. 마우스를 올리면 밑줄이 중앙에서 시작해 양옆으로 동시에 퍼지며 그어집니다.
.link::after {
transform-origin: center;
transition: transform 300ms $ease-pop;
}
.link:hover::after,
.link:focus-visible::after {
transform: scaleX(1);
}
03여러 줄 대응 밑줄
메뉴 글자가 길어 두 줄로 줄바꿈되면 의사요소 밑줄은 보통 마지막 줄에만 그어집니다. 배경 그러데이션을 밑줄로 쓰고 줄마다 통째로 복제하는 속성을 더해, 두 줄로 감싸진 링크도 줄마다 따로 밑줄이 그어지게 했습니다.
.wrap { width: 160px; text-align: center; }
.link {
background-image: linear-gradient(currentColor, currentColor);
background-repeat: no-repeat;
background-position: 0 100%;
background-size: 0% 2px;
box-decoration-break: clone;
-webkit-box-decoration-break: clone;
transition: background-size 300ms $ease-out;
}
.link:hover, .link:focus-visible {
background-size: 100% 2px;
}
04굵어지는 밑줄
네이티브 밑줄 속성을 그대로 쓰고 두께와 밑줄·글자 사이 간격만 애니메이션했습니다. 레이아웃을 계산하는 속성이 아니라서 주변 글자가 밀리지 않으면서 밑줄만 눈에 띄게 굵어집니다.
.link {
text-decoration-line: underline;
text-decoration-style: solid;
text-decoration-thickness: 1px;
text-underline-offset: 3px;
transition: text-decoration-thickness 300ms $ease-out,
text-underline-offset 300ms $ease-out;
}
.link:hover, .link:focus-visible {
text-decoration-thickness: 4px;
text-underline-offset: 5px;
}
지원 범위는 MDN text-decoration-thickness 문서에 정리돼 있습니다.
05색이 흐르는 밑줄
밑줄 막대에 두 배 폭의 그러데이션을 깔아 두면, 호버 중에만 그 그러데이션이 옆으로 흘러 색이 지나가는 것처럼 보입니다. 마우스를 떼면 흐름이 멈추고 밑줄도 다시 줄어듭니다.
.link::after {
background: linear-gradient(90deg, $color, $stage-yellow, $color);
background-size: 200% 100%;
transform: scaleX(0);
transform-origin: left center;
transition: transform 220ms $ease-out;
}
.link:hover::after, .link:focus-visible::after {
transform: scaleX(1);
animation: color-flow 1.4s linear infinite;
}
@keyframes color-flow {
0% { background-position: 0% 0; }
100% { background-position: 100% 0; }
}
06물결 밑줄
네이티브 물결 밑줄 대신 두 겹의 원형 그러데이션을 반복시켜 물결 모양을 직접 그렸습니다. 호버하면 물결이 드러나면서 동시에 옆으로 흘러가, 멈춰 있는 물결이 아니라 계속 흐르는 물결이 됩니다.
.link::after {
background-image:
radial-gradient(circle at 10px -5px, transparent 12px, currentColor 13px, currentColor 14px, transparent 15px),
radial-gradient(circle at 30px 5px, transparent 12px, currentColor 13px, currentColor 14px, transparent 15px);
background-repeat: repeat-x;
background-size: 40px 10px;
opacity: 0;
transition: opacity 220ms $ease-out;
}
.link:hover::after, .link:focus-visible::after {
opacity: 1;
animation: wave-flow 1.2s linear infinite;
}
@keyframes wave-flow {
0% { background-position: 0 0, 0 0; }
100% { background-position: -40px 0, -40px 0; }
}
07호버 취소선
글자 중간 높이에 가로선을 두고 호버할 때만 긋습니다. 자동으로 반복되지 않고 오직 마우스를 올렸을 때만 나타나므로, 삭제나 완료 처리처럼 사용자가 직접 일으키는 동작을 암시하기에 알맞습니다.
.link::after {
content: "";
position: absolute;
left: 0; right: 0; top: 50%;
height: 2px;
background: currentColor;
transform: translateY(-50%) scaleX(0);
transform-origin: left center;
transition: transform 140ms $ease-pop;
}
.link:hover::after, .link:focus-visible::after {
transform: translateY(-50%) scaleX(1);
}
08호버 형광펜
글자 뒤에 살짝 기울인 둥근 블록을 숨겨 두고, 호버하면 형광펜으로 방금 칠한 것처럼 차오릅니다. 겹치는 색을 곱하는 방식으로 섞어서 실제 마커 특유의 반투명한 느낌을 냅니다.
.link {
position: relative;
isolation: isolate;
&::before {
content: "";
position: absolute;
inset: 10% -4% 6%;
background: $stage-yellow;
border-radius: $r-xs;
transform: skewX(-6deg) scaleX(0);
transform-origin: left center;
mix-blend-mode: multiply;
z-index: -1;
transition: transform 300ms $ease-pop;
}
&:hover::before, &:focus-visible::before {
transform: skewX(-6deg) scaleX(1);
}
}
색을 곱해서 섞는 것 말고도 쓸 수 있는 다른 값들은 mix-blend-mode 문서에 정리돼 있습니다.
09점선→실선 변형
밑줄 스타일 자체는 애니메이션이 안 되는 값이라, 점선 막대와 실선 막대를 같은 자리에 겹쳐 두고 호버할 때 서로 투명도를 바꿔치기합니다. 아직 안 읽은 링크가 확정된 상태로 바뀌는 느낌을 줍니다.
.link::before, .link::after {
content: "";
position: absolute;
left: 0; right: 0; bottom: 0;
height: 2px;
transition: opacity 300ms $ease-out;
}
.link::before {
background-image: repeating-linear-gradient(90deg, currentColor 0 3px, transparent 3px 7px);
opacity: 1;
}
.link::after { background: currentColor; opacity: 0; }
.link:hover::before, .link:focus-visible::before { opacity: 0; }
.link:hover::after, .link:focus-visible::after { opacity: 1; }
어디서 깨지는가 — 함정 둘
아홉 개 중 하나(03 여러 줄 대응 밑줄)는 처음 만들었을 때 밑줄이 자라는 게 전혀 안 보였습니다. 밑줄을 그리는 배경 그림을 옆으로 반복시켜 뒀더니, 크기가 아주 작은 값이어도 타일이 계속 반복돼 밑줄이 시작부터 이미 꽉 차 있는 것처럼 보였습니다. 반복을 끄고 한 번만 그리게 바꾸자 각 줄의 밑줄이 실제로 0%에서 100%까지 자라는 게 렌더 측정에서도, 눈으로도 확인됐습니다.
같은 항목에서 두 번째 함정도 겹쳐 나왔습니다. 두 줄로 감싸지게 하려고 부모 요소에 최대 너비를 줬는데, 가운데 정렬 레이아웃 안에서는 그 값이 실제로 적용되지 않고 글자가 원래 폭 그대로 한 줄에 펼쳐졌습니다. 최대 너비 대신 고정된 너비 값으로 바꾸고서야 글자가 두 줄로 감싸지고 밑줄도 줄마다 따로 그려지는 걸 캡처한 이미지로 확인했습니다. 아홉 개 파일이 담긴 압축 파일의 비밀번호는 je9m2xxs이고, 문장 속에 평범하게 적어 두었으니 여기서 그대로 복사하면 됩니다.
접근성
OS에서 모션 줄이기를 켠 사람에게는 아홉 항목 모두 prefers-reduced-motion: reduce 안에서 전환 자체를 없앱니다. 그래도 실제 호버·포커스 상태 변화는 그대로 남겨 뒀습니다 — 01·02·03·04·07·08·09는 마우스를 올리는 순간 애니메이션 없이 곧바로 최종 모습(밑줄이 그어진, 두꺼워진, 형광펜이 칠해진 상태)으로 바뀌고, 05·06은 색이 흐르거나 물결이 움직이는 동작만 꺼지고 밑줄·물결이 드러나는 것 자체는 남습니다. prefers-reduced-motion 문서에 어느 브라우저부터 지원하는지 정리돼 있습니다.
링크 밑줄 말고 다른 CSS 애니메이션은 텍스트 강조 애니메이션 9종이나 버튼 애니메이션 9종에서 더 찾을 수 있습니다.
FAQ
아홉 개를 한 페이지에 같이 써도 되나요?
가능하지만 같은 화면 안에서는 내비게이션엔 하나, 본문 인라인 링크엔 하나처럼 자리별로 한 가지만 쓰는 게 좋습니다. 여러 밑줄 효과가 뒤섞이면 어떤 게 지금 가리키는 링크인지 오히려 헷갈립니다.
방문한 링크 색도 같이 바꿀 수 있나요?
네, :visited 선택자에 color 값을 따로 주면 됩니다. 다만 :visited 에서는 보안상 색·투명도 계열 속성만 허용되므로, 이 글의 transform·background 기반 밑줄 애니메이션은 방문 여부와 상관없이 항상 같은 모양으로 동작합니다.
밑줄이 텍스트 줄바꿈 위치에 따라 어긋나 보이는 이유는 뭔가요?
03 여러 줄 대응 밑줄처럼 box-decoration-break: clone 을 쓰지 않았다면 의사요소 밑줄은 보통 링크 전체의 마지막 줄 아래에만 그려집니다. 줄마다 밑줄이 필요하면 의사요소 대신 배경 이미지 방식으로 바꾸고 box-decoration-break: clone 을 더하면 해결됩니다.