CSS 이미지 호버 효과 9종 — 한 줄 복붙으로
이미지 호버 효과는 마우스를 올리거나 초점을 맞췄을 때 사진이 어떻게 반응하는지 정하는 짧은 전환입니다. 아홉 개 중 하나(07)는 사진 두 장을 겹쳐 원 모양 창을 키워 드러내는데, CSS에는 그 반지름을 끝까지 키우는 코너 키워드가 없어서 값을 직접 잡아야 합니다.
자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip
아홉 개는 인기 순이 아니라 사진이 카드 안에서 하는 일 순서로 놓았습니다. 존재감을 키우는 것(01~02), 정보를 더 얹는 것(03~04), 입체감과 고급스러움을 더하는 것(05~06), 다른 사진으로 갈아 끼우는 것(07~08), 마지막으로 시선을 붙잡는 것(09)입니다. 데모는 실제 사진 파일 없이 CSS 그러데이션으로 "사진 자리"만 채웠으니, 실서비스에 쓸 때는 배경 대신 <img> 하나만 끼워 넣으면 됩니다.
01확대 줌인
사진을 감싼 카드에 overflow: hidden을 걸고 안쪽 사진만 transform: scale로 14% 키웁니다. 카드 테두리는 그대로라 사진만 도드라져 보입니다.
.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)으로 트랜지션됩니다. 원래 색이 서서히 되살아나는 것처럼 보입니다.
.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)으로 밀려 올라오며 제목과 설명을 드러냅니다. 캡션은 콘텐츠라 터치 기기에선 기본으로 보이게 했습니다.
.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로 트랜지션되며 덮이고, 가운데 "자세히 보기" 문구가 함께 떠오릅니다.
.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로 살짝 기운 카드가 입체적으로 들리는 것처럼 보입니다.
.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로 한 번 훑고 지나가게 합니다.
.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%)를 걸어 숨긴 뒤, 호버 시 반지름을 키워 원 모양 창이 확대되며 드러납니다.
.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로 트랜지션해 다른 장면으로 바꿉니다.
.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를 몇 픽셀씩 왕복시켜, 호버·포커스·클릭 시 사진이 빠르게 진동하며 시선을 붙잡습니다.
@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-side와 farthest-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__pic의 background를 지우고 <img> 태그를 넣거나, object-fit: cover를 건 이미지로 바꾸면 코드는 그대로 동작합니다.
아홉 개를 한 페이지에 다 써도 성능에 문제없습니까?
대부분은 transform과 opacity만 바뀌어 레이아웃을 다시 계산하지 않지만, 02의 filter와 07의 clip-path는 픽셀을 다시 그리는 비용이 조금 더 듭니다. 화면에 동시에 보이는 카드가 수십 장을 넘어가면 이 두 효과만 골라 아끼는 게 낫습니다.
모바일에서도 호버와 똑같이 보입니까?
호버가 아니라 탭(:active)이 트리거가 됩니다. 03처럼 콘텐츠를 가리는 효과는 @media (hover: none)에서 기본으로 펼쳐 두었고, 나머지는 탭하는 동안만 효과가 보였다가 손을 떼면 원래대로 돌아갑니다.