GODRICH

CSS 밑줄 애니메이션 9종 — 한 줄 복붙으로

css 밑줄 애니메이션은 마우스를 올리거나 키보드로 초점을 옮겼을 때만 링크 아래 줄이 움직이며 나타나는 효과로, 지금 가리키고 있는 링크가 어디인지 눈으로 바로 알려줍니다. 아홉 가지를 모았고 전부 한 줄 복붙으로 그대로 붙여 쓸 수 있습니다.

자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip

아홉 개는 인기 순이 아니라 링크를 마주치는 순서대로 놓았습니다. 가장 기본이 되는 좌우 밑줄(01)에서 시작해 같은 방식의 변형(02)을 보여주고, 실제로 메뉴 글자가 줄바꿈되는 상황(03)을 짚은 뒤, 더 강한 강조(04)와 브랜드 색(05)을 더합니다. 이어서 장난스러운 물결(06)로 톤을 풀고, 삭제·완료 같은 상태 변화를 암시하는 취소선(07), 추천 링크를 표시하는 형광펜(08), 마지막으로 아직 안 읽은 링크가 확정 상태로 바뀌는 점선→실선(09)까지 이어집니다.

왜 :hover 뿐 아니라 :focus-visible 도 같이 걸어야 하나요

마우스가 없는 키보드 사용자는 Tab 키로 링크 사이를 옮겨 다닙니다. 아홉 개 전부 :hover 옆에 :focus-visible 을 나란히 걸어, 키보드로 초점이 옮겨 왔을 때도 마우스를 올린 것과 똑같이 밑줄이 움직이게 했습니다. 갤러리에 렌더된 데모는 .is-demo 클래스가 같은 모양을 2초마다 강제로 반복해 보여줄 뿐이고, 실제 페이지에 붙이면 진짜 호버·포커스에서만 움직입니다.

01좌→우 밑줄

가장 기본이 되는 형태로, 링크 아래 얇은 막대가 왼쪽에서 시작해 오른쪽으로 자라며 그어집니다. 마우스를 떼면 같은 방향으로 다시 줄어들어, 들어올 때와 나갈 때가 대칭을 이룹니다.

::afterscaleXtransform-origin:left
.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과 같은 막대를 쓰되 기준점만 가운데로 옮겼습니다. 마우스를 올리면 밑줄이 중앙에서 시작해 양옆으로 동시에 퍼지며 그어집니다.

::afterscaleXtransform-origin:center
.link::after {
  transform-origin: center;
  transition: transform 300ms $ease-pop;
}
.link:hover::after,
.link:focus-visible::after {
  transform: scaleX(1);
}

03여러 줄 대응 밑줄

메뉴 글자가 길어 두 줄로 줄바꿈되면 의사요소 밑줄은 보통 마지막 줄에만 그어집니다. 배경 그러데이션을 밑줄로 쓰고 줄마다 통째로 복제하는 속성을 더해, 두 줄로 감싸진 링크도 줄마다 따로 밑줄이 그어지게 했습니다.

background-sizebox-decoration-break:clonebackground-position: 0 100%
.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굵어지는 밑줄

네이티브 밑줄 속성을 그대로 쓰고 두께와 밑줄·글자 사이 간격만 애니메이션했습니다. 레이아웃을 계산하는 속성이 아니라서 주변 글자가 밀리지 않으면서 밑줄만 눈에 띄게 굵어집니다.

text-decoration-thicknesstext-underline-offsetsolid
.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색이 흐르는 밑줄

밑줄 막대에 두 배 폭의 그러데이션을 깔아 두면, 호버 중에만 그 그러데이션이 옆으로 흘러 색이 지나가는 것처럼 보입니다. 마우스를 떼면 흐름이 멈추고 밑줄도 다시 줄어듭니다.

background-positionlinear-gradient호버 루프
.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물결 밑줄

네이티브 물결 밑줄 대신 두 겹의 원형 그러데이션을 반복시켜 물결 모양을 직접 그렸습니다. 호버하면 물결이 드러나면서 동시에 옆으로 흘러가, 멈춰 있는 물결이 아니라 계속 흐르는 물결이 됩니다.

radial-gradientbackground-repeat: repeat-x호버 루프
.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호버 취소선

글자 중간 높이에 가로선을 두고 호버할 때만 긋습니다. 자동으로 반복되지 않고 오직 마우스를 올렸을 때만 나타나므로, 삭제나 완료 처리처럼 사용자가 직접 일으키는 동작을 암시하기에 알맞습니다.

::aftertop:50%scaleX
.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호버 형광펜

글자 뒤에 살짝 기울인 둥근 블록을 숨겨 두고, 호버하면 형광펜으로 방금 칠한 것처럼 차오릅니다. 겹치는 색을 곱하는 방식으로 섞어서 실제 마커 특유의 반투명한 느낌을 냅니다.

skewXmix-blend-modescaleX
.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점선→실선 변형

밑줄 스타일 자체는 애니메이션이 안 되는 값이라, 점선 막대와 실선 막대를 같은 자리에 겹쳐 두고 호버할 때 서로 투명도를 바꿔치기합니다. 아직 안 읽은 링크가 확정된 상태로 바뀌는 느낌을 줍니다.

::before·::afteropacity 교차점선→실선
.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 을 더하면 해결됩니다.

압축 비밀번호를 입력하세요

비밀번호는 이 글 본문 안에 있습니다. 읽다 보면 나옵니다.