GODRICH

CSS 이미지 호버 효과 9종 — 한 줄 복붙으로

이미지 호버 효과는 마우스를 올리거나 초점을 맞췄을 때 사진이 어떻게 반응하는지 정하는 짧은 전환입니다. 아홉 개 중 하나(07)는 사진 두 장을 겹쳐 원 모양 창을 키워 드러내는데, CSS에는 그 반지름을 끝까지 키우는 코너 키워드가 없어서 값을 직접 잡아야 합니다.

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

아홉 개는 인기 순이 아니라 사진이 카드 안에서 하는 일 순서로 놓았습니다. 존재감을 키우는 것(01~02), 정보를 더 얹는 것(03~04), 입체감과 고급스러움을 더하는 것(05~06), 다른 사진으로 갈아 끼우는 것(07~08), 마지막으로 시선을 붙잡는 것(09)입니다. 데모는 실제 사진 파일 없이 CSS 그러데이션으로 "사진 자리"만 채웠으니, 실서비스에 쓸 때는 배경 대신 <img> 하나만 끼워 넣으면 됩니다.

01확대 줌인

사진을 감싼 카드에 overflow: hidden을 걸고 안쪽 사진만 transform: scale로 14% 키웁니다. 카드 테두리는 그대로라 사진만 도드라져 보입니다.

transform:scaleoverflow:hidden0.3초
.photo { overflow: hidden; }
.photo__pic {
  transform: scale(1);
  transition: transform $duration $easing;
}
.photo:hover .photo__pic,
.photo:focus-visible .photo__pic,
.photo:active .photo__pic {
  transform: scale(1.14);
}

02흑백→컬러

filter: grayscale(1)로 채도를 뺀 사진이 호버·포커스 시 grayscale(0)으로 트랜지션됩니다. 원래 색이 서서히 되살아나는 것처럼 보입니다.

filter:grayscaletransition0.4초
.photo__pic {
  filter: grayscale(1);
  transition: filter $duration $easing;
}
.photo:hover .photo__pic,
.photo:focus-visible .photo__pic,
.photo:active .photo__pic {
  filter: grayscale(0);
}

03캡션 슬라이드

사진 아래 숨어 있던 캡션 바가 translateY(100%)에서 translateY(0)으로 밀려 올라오며 제목과 설명을 드러냅니다. 캡션은 콘텐츠라 터치 기기에선 기본으로 보이게 했습니다.

translateYoverflow:hidden0.25초
.photo__cap {
  position: absolute;
  inset: auto 0 0 0;
  transform: translateY(100%);
  transition: transform $duration $easing;
}
.photo:hover .photo__cap,
.photo:focus-visible .photo__cap,
.photo:active .photo__cap {
  transform: translateY(0);
}
@media (hover: none) {
  .photo__cap { transform: translateY(0); }
}

04오버레이 페이드

사진 위에 반투명 색 레이어가 opacity: 0에서 1로 트랜지션되며 덮이고, 가운데 "자세히 보기" 문구가 함께 떠오릅니다.

opacity::after0.3초
.photo__overlay {
  position: absolute;
  inset: 0;
  background: rgba($color, .82);
  opacity: 0;
  transition: opacity $duration $easing;
}
.photo:hover .photo__overlay,
.photo:focus-visible .photo__overlay,
.photo:active .photo__overlay {
  opacity: 1;
}

053D 틸트

부모에 perspective를 주고 카드에 transform-style: preserve-3d를 걸면, 호버 시 rotateX·rotateY로 살짝 기운 카드가 입체적으로 들리는 것처럼 보입니다.

perspectiverotateX/rotateYtransform-style
.frame { perspective: 700px; }
.photo {
  transform-style: preserve-3d;
  transform: rotateX(0) rotateY(0);
  transition: transform $duration $easing;
}
.photo:hover,
.photo:focus-visible,
.photo:active {
  transform: rotateX(-9deg) rotateY(11deg) translateY(-4px);
}

06밝기 스윕

대각선으로 기운 밝은 띠를 그러데이션으로 그려 화면 밖에 두었다가, 호버 시 transform: translateX로 한 번 훑고 지나가게 합니다.

linear-gradientbackground-position0.6초
.photo__sweep {
  position: absolute;
  inset: 0;
  background: linear-gradient(100deg, transparent 30%, rgba($subject-cream, .65) 50%, transparent 70%);
  transform: translateX(-120%);
}
.photo:hover .photo__sweep,
.photo:focus-visible .photo__sweep,
.photo:active .photo__sweep {
  transform: translateX(120%);
  transition: transform $duration $easing;
}

07클립 원형 확대

사진 두 장을 겹쳐두고 위쪽 사진에 clip-path: circle(0% at 50% 50%)를 걸어 숨긴 뒤, 호버 시 반지름을 키워 원 모양 창이 확대되며 드러납니다.

clip-path:circle()반지름 확장0.5초
.photo__pic {
  clip-path: circle(0% at 50% 50%);
  transition: clip-path $duration $easing;
}
.photo:hover .photo__pic,
.photo:focus-visible .photo__pic,
.photo:active .photo__pic {
  clip-path: circle(150% at 50% 50%);
}

08두 장 교차

같은 자리에 사진 두 장을 절대 위치로 겹쳐두고, 두 번째 사진의 opacity를 0에서 1로 트랜지션해 다른 장면으로 바꿉니다.

opacity 교차position:absolute0.4초
.photo__a,
.photo__b {
  position: absolute;
  inset: 0;
}
.photo__b {
  opacity: 0;
  transition: opacity $duration $easing;
}
.photo:hover .photo__b,
.photo:focus-visible .photo__b,
.photo:active .photo__b {
  opacity: 1;
}

09흔들림

짧은 @keyframes로 좌우 translateX를 몇 픽셀씩 왕복시켜, 호버·포커스·클릭 시 사진이 빠르게 진동하며 시선을 붙잡습니다.

@keyframestranslateX0.4초
@keyframes shake {
  0%, 100% { transform: translateX(0); }
  20% { transform: translateX(-6px); }
  40% { transform: translateX(5px); }
  60% { transform: translateX(-4px); }
  80% { transform: translateX(3px); }
}
.photo:hover .photo__pic,
.photo:focus-visible .photo__pic,
.photo:active .photo__pic {
  animation: shake $duration $easing;
}

어디서 깨지는가 — 함정 둘

07의 clip-path: circle()은 만만해 보이지만 반지름 키워드에 함정이 있습니다. radial-gradient()에는 farthest-corner가 있지만, circle()의 반지름 키워드는 closest-sidefarthest-side뿐입니다. farthest-corner를 그대로 썼더니 브라우저가 값 전체를 무효로 처리해 원이 한 프레임도 움직이지 않는 걸 실측했습니다. MDN의 basic-shape 문서circle()이 받는 키워드 목록을 정리해 둡니다. 반지름을 150%처럼 넉넉한 퍼센트 값으로 바꾸자 원이 카드 구석까지 제대로 덮였습니다. circle()ellipse()radial-gradient()와 이름이 비슷해도 받는 키워드 집합이 다르다는 걸 이번에 확실히 배웠고, caniuse의 clip-path 지원표를 같이 열어 반지름 값이 브라우저마다 다르게 처리되지 않는지도 확인해 두었습니다.

또 하나는 03 캡션 슬라이드처럼 사진 위에 숨겨두는 콘텐츠입니다. 호버가 없는 터치 기기에서 @media (hover: none)을 빼먹으면 캡션은 영영 안 보이는 텍스트가 됩니다. caniuse의 hover 미디어 특성 지원표를 보면 대부분의 모바일 브라우저가 이 값을 지원하니, 폴백 없이 방치할 이유가 없습니다.

이 아홉 개 파일을 실제로 렌더해 값을 확인한 압축 파일의 비밀번호는 pbk89rn6이고, 아래 버튼으로 받을 수 있습니다.

접근성

아홉 개 전부 prefers-reduced-motion: reduce를 처리해서, 운영체제에서 "동작 줄이기"를 켠 사람에게는 트랜지션·애니메이션이 꺼지고 마지막 상태만 남습니다. 01·02·04·06·08은 즉시 확대·컬러·오버레이·스윕·교차 상태로 붙고, 03·07처럼 콘텐츠를 가리는 효과는 아예 기본으로 펼쳐 둡니다. :hover뿐 아니라 :focus-visible:active에도 같은 스타일을 걸어서 키보드 사용자와 터치 사용자가 마우스 사용자와 같은 결과를 봅니다. 이 조합은 MDN의 @media prefers-reduced-motion 문서에 정리돼 있습니다.

@media (prefers-reduced-motion: reduce) {
  .photo__pic { transition: none; }
  .photo.is-demo .photo__pic { animation: none; }
}

이 아홉 개 말고 CSS 로 만드는 다른 전환·등장 효과가 궁금하면 CSS 카테고리 페이지를 열어 보시고, 사이트 소개는 여기에서 확인하십시오.

FAQ

이미지 호버 효과에 실제 사진 파일이 꼭 있어야 합니까?

아닙니다. 이 글의 데모 아홉 개는 전부 CSS 그러데이션과 도형만으로 "사진 자리"를 만들었습니다. 실제로 쓸 때는 .photo__picbackground를 지우고 <img> 태그를 넣거나, object-fit: cover를 건 이미지로 바꾸면 코드는 그대로 동작합니다.

아홉 개를 한 페이지에 다 써도 성능에 문제없습니까?

대부분은 transformopacity만 바뀌어 레이아웃을 다시 계산하지 않지만, 02의 filter와 07의 clip-path는 픽셀을 다시 그리는 비용이 조금 더 듭니다. 화면에 동시에 보이는 카드가 수십 장을 넘어가면 이 두 효과만 골라 아끼는 게 낫습니다.

모바일에서도 호버와 똑같이 보입니까?

호버가 아니라 탭(:active)이 트리거가 됩니다. 03처럼 콘텐츠를 가리는 효과는 @media (hover: none)에서 기본으로 펼쳐 두었고, 나머지는 탭하는 동안만 효과가 보였다가 손을 떼면 원래대로 돌아갑니다.

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

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