GODRICH
#css애니메이션#버튼#로딩#전환효과#스크롤#대시보드#실전ui#디자인트렌드

CSS 이미지 슬라이드 애니메이션 9종

사진 위에 마우스를 올리면 커튼처럼 열리거나 원이 점점 커지며 드러나는 걸 본 적 있으신가요? 이런 CSS 이미지 슬라이드 애니메이션을 한 줄 복붙으로 쓸 수 있게 아홉 가지로 모았고, 그중 하나는 자바스크립트 없이 필터 탭까지 만듭니다.

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

아홉 개는 화려한 순서가 아니라 갤러리를 둘러보는 방문자가 실제로 거치는 순서로 배치했습니다. 먼저 사진 한 장이 덮개·커튼·원형 마스크로 드러나는 단일 리빌 기법(01~03)을 지나, 여러 장이 한꺼번에 놓인 그리드가 순서대로 나타나거나(04) 확대되어 보이는 순간(05, 07)을 거칩니다. 그다음은 개별 칸에 정보를 얹는 호버(06)와 스크롤을 따라 드러나는 큰 사진(08)을 지나, 마지막은 여러 장을 카테고리별로 골라보는 필터(09)로 마무리됩니다. 슬라이드로 드러나는 값의 종류가 금액이라면 CSS 가격표 애니메이션이 그 숫자를 다루고, 반투명한 판 위에서 같은 일을 하려면 글래스모피즘 효과가 재료입니다.

01좌우 와이프

이미지 갤러리 css 에서 어디에 쓰나

사진을 덮은 판이 오른쪽 끝을 축으로 폭이 점점 줄어들면서, 왼쪽부터 오른쪽으로 사진이 드러납니다.

좌우 와이프
새 창에서 열기

01 · 좌우 와이프 — 측정 누적면적 29.128% · 움직인 프레임 18/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

scaleXtransform-origin500ms

코드의 요점

transform-origin을 오른쪽에 고정하고 scaleX만 움직이는 게 핵심입니다.

.wipe-cover {
  position: absolute;
  inset: 0;
  background: #fff7e6;
  transform-origin: 100% 50%;
  transform: scaleX(1);
  transition: transform 600ms ease-out;
}
.wipe-focal:hover .wipe-cover {
  transform: scaleX(0);
}

02커튼 분할 리빌

화면에서 벌어지는 일

반으로 나뉜 두 패널이 사진을 덮고 있다가, 마우스를 올리면 왼쪽은 왼쪽으로 오른쪽은 오른쪽으로 완전히 밀려나며 무대 커튼처럼 열립니다.

커튼 분할 리빌
새 창에서 열기

02 · 커튼 분할 리빌 — 측정 누적면적 29.122% · 움직인 프레임 18/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

translateX50vw600ms

동작 원리

.curtain-l { left: 0; }
.curtain-r { left: 50%; }
.curtain-l, .curtain-r {
  position: absolute;
  top: 0; bottom: 0;
  width: 50%;
  transform: translateX(0);
  transition: transform 600ms cubic-bezier(.2, .8, .2, 1);
}
.gallery-focal:hover .curtain-l { transform: translateX(-100%); }
.gallery-focal:hover .curtain-r { transform: translateX(100%); }

03원형 마스크 확장

어떻게 보이나

사진 정중앙에서 시작한 원이 반지름 0%에서 75%까지 커지며, 카메라 조리개가 열리듯 사진을 드러냅니다.

원형 마스크 확장
새 창에서 열기

03 · 원형 마스크 확장 — 측정 누적면적 29.122% · 움직인 프레임 17/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

clip-pathcircle()550ms

만드는 방식

.circle-photo {
  clip-path: circle(0% at 50% 50%);
  transition: clip-path 600ms ease-out;
}
.circle-focal:hover .circle-photo {
  clip-path: circle(75% at 50% 50%);
}

04메이슨리 순차 페이드

언제 쓰나

높낮이가 다른 사진 타일이 벽돌처럼 쌓인 그리드가 화면에 들어오면, 타일마다 순서대로 나타납니다.

메이슨리 순차 페이드
새 창에서 열기

04 · 메이슨리 순차 페이드 — 측정 누적면적 25.641% · 움직인 프레임 12/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

columnstransition-delayJS 7줄

왜 이렇게 만들었나

타일마다 다른 건 transition-delay 값 하나뿐입니다.

var tiles = document.querySelectorAll('.masonry-tile');
var io = new IntersectionObserver(function (entries) {
  entries.forEach(function (e) {
    if (e.isIntersecting) { e.target.classList.add('in'); io.unobserve(e.target); }
  });
}, { threshold: 0.3 });
tiles.forEach(function (t) { io.observe(t); });

05dialog 라이트박스

어떤 화면인가

썸네일 버튼을 누르면 네이티브 dialog가 showModal()로 열립니다.

dialog 라이트박스
새 창에서 열기

05 · dialog 라이트박스 — 측정 누적면적 70.732% · 움직인 프레임 8/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

dialogshowModalJS 6줄

구현의 핵심

닫기 버튼은 페이지가 갑자기 스크롤되지 않도록 preventScroll 옵션과 함께 포커스를 받습니다.

var dlg = document.querySelector('.lb-dialog');
var btn = document.querySelector('.lb-close');
thumb.addEventListener('click', function () {
  dlg.showModal();
  btn.focus({ preventScroll: true });
});
btn.addEventListener('click', function () { dlg.close(); });

06호버 캡션 그리드

무엇이 움직이나

세 장이 나란히 놓인 그리드에서 지금 가리킨 칸의 캡션만 아래에서 위로 올라오고 서서히 나타납니다.

호버 캡션 그리드
새 창에서 열기

06 · 호버 캡션 그리드 — 측정 누적면적 4.412% · 움직인 프레임 11/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

translateYopacity300ms

무엇이 핵심인가

나머지 칸은 그대로 있어 어느 사진을 보고 있는지 헷갈리지 않습니다.

.caption-bar {
  transform: translateY(100%);
  opacity: 0;
  transition: transform 300ms ease-out, opacity 300ms ease-out;
}
.gallery-tile:hover .caption-bar {
  transform: translateY(0);
  opacity: 1;
}

07썸네일→라이트박스 모핑

어디에 쓰나

썸네일을 클릭하면 그 위치를 getBoundingClientRect()로 잰 뒤, 오버레이 패널을 그 자리에 먼저 놓고 transform을 지워 확대된 자리까지 부드럽게 전환합니다.

썸네일→라이트박스 모핑
새 창에서 열기

07 · 썸네일→라이트박스 모핑 — 측정 누적면적 21.074% · 움직인 프레임 16/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

getBoundingClientRectFLIPJS 9줄

짚어 둘 점

FLIP이라 불리는 이 기법은 transform만 쓰므로 브라우저를 가리지 않습니다.

var t = thumb.getBoundingClientRect();
var p = panel.getBoundingClientRect();
panel.style.transform =
  'translate(' + (t.left - p.left) + 'px,' + (t.top - p.top) + 'px) ' +
  'scale(' + (t.width / p.width) + ',' + (t.height / p.height) + ')';
panel.classList.add('is-open');
requestAnimationFrame(function () {
  panel.style.transform = 'translate(0,0) scale(1,1)';
});

08스크롤 마스크 리빌

눈에 보이는 것

사진이 화면에 들어오면 mask-image가 위치를 오른쪽에서 왼쪽으로 쓸며 지나가, 딱딱한 경계 없이 부드러운 가장자리로 드러납니다.

스크롤 마스크 리빌
새 창에서 열기

08 · 스크롤 마스크 리빌 — 측정 누적면적 29.122% · 움직인 프레임 16/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

mask-imagemask-positionJS 7줄

어떻게 만들었나

트리거는 IntersectionObserver가 붙이는 클래스 하나뿐입니다.

.mask-photo {
  mask-image: linear-gradient(100deg, transparent 0 38%, #000 58% 100%);
  mask-size: 260% 100%;
  mask-position: 0 0;
  transition: mask-position 700ms ease-out;
}
.mask-frame.in .mask-photo {
  mask-position: 100% 0;
}

09:has 기반 필터 탭

어떤 상황인가

숨겨진 라디오 버튼 3개와 :has() 셀렉터만으로, 탭을 고르면 다른 카테고리 타일이 옅어지고 살짝 줄어듭니다.

:has 기반 필터 탭
새 창에서 열기

09 · :has 기반 필터 탭 — 측정 누적면적 24.351% · 움직인 프레임 13/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

:has()translateXJS 8줄

코드 읽는 법

자바스크립트 없이도 진짜로 동작하는 필터입니다.

.gallery:has(#f-photo:checked) .tile:not([data-cat="photo"]) {
  opacity: .15;
  transform: scale(.94);
  pointer-events: none;
}
.gallery:has(#f-video:checked) .tile:not([data-cat="video"]) {
  opacity: .15;
  transform: scale(.94);
  pointer-events: none;
}

CSS 이미지 슬라이드 애니메이션은 어디서 깨지는가

무엇이 문제인가

라이트박스는 이미지 슬라이드 효과와 자주 헷갈리는데, 그중에서도 가장 자주 오해하는 지점은 05번과 07번입니다. 라이트박스를 만들 때 dialog가 열리는 순간에 @starting-style로 멋진 등장 트랜지션을 넣고 싶어지지만, 이 기능은 Safari와 Firefox의 지원이 아직 들쭉날쭉해 이 글의 데모에서는 쓰지 않았습니다 — 대신 dialog는 브라우저 기본 방식으로 즉시 열리고, ::backdrop도 애니메이션 없이 정적인 반투명 막 하나로만 남겨 뒀습니다. 07번 역시 이름과 달리 document.startViewTransition() API를 쓰지 않는데, 이 API도 브라우저마다 지원과 결과 화면이 갈리기 때문입니다.

어떻게 피하나

뒤에 깔린 사진이 앞글자를 해치는 판단은 글자가 스스로 움직이는 키네틱 타이포그래피 랜딩에서 배경 가독성 문제로 되돌아옵니다.

대신 클릭한 순간의 좌표를 재서 transform만으로 확대되는 자리까지 옮기는 FLIP 기법을 썼습니다. 04번과 08번은 둘 다 실제 상호작용은 IntersectionObserver가 맡고, 캡처용 반복 재생만 별도의 @keyframes 경로로 흉내 낸다는 공통점이 있으며, 아홉 항목의 vanilla 코드와 zip 압축 비밀번호는 63vswp74 이니 이 문단을 천천히 읽으면 찾을 수 있습니다.

접근성 — 아홉 개가 reduced-motion에서 남기는 것

reduced-motion 에서 남는 것

시스템에서 동작 줄이기를 켜면 아홉 개 전부 애니메이션과 트랜지션을 끕니다. 01·02·03·06·07처럼 사진을 꾸며서 보여주는 반응은 움직임 없이 최종 모습(사진이 드러난 상태)으로 곧장 고정해도 정보 손실이 없습니다. 04(메이슨리 순차 등장)와 08(스크롤 마스크)도 순서나 경계가 움직임 자체에 담긴 정보는 아니라서, 타일과 사진을 처음부터 전부 드러난 상태로 두면 충분합니다. 다만 09(필터 탭)는 다릅니다 — 어떤 탭이 눌렸는지 자체가 정보라서 애니메이션을 꺼도 opacity·transform 값은 즉시 바뀌어야 하고, 실제로 이 항목은 :has()가 상태를 판정하는 구조라 트랜지션만 없앨 뿐 필터링 자체는 항상 그대로 작동합니다.

이어서 볼 글

리빌 말고 사진 자체가 반응하는 호버 효과를 찾는다면 CSS 이미지 호버 효과 글도 참고할 만하고, 전체 목록은 CSS 애니메이션 카테고리에서 모아 볼 수 있습니다. clip-path의 세부 문법은 MDN clip-path 문서에, :has() 셀렉터의 브라우저 지원 범위는 MDN :has() 문서에 정리돼 있습니다.

FAQ — 자주 묻는 질문

css 이미지 애니메이션 효과 없이 사진이 그냥 나타나면 안 되나요?

꼭 써야 하는 건 아니지만, 사진이 서서히 혹은 흥미로운 방식으로 드러나면 방문자의 시선이 그 순간에 머물러 포트폴리오나 상품 갤러리에서 클릭·호버를 유도하는 신호가 됩니다. 01~03처럼 단일 사진에 쓰는 가벼운 효과부터 07처럼 클릭까지 이어지는 효과까지, 화면 목적에 맞춰 고르면 됩니다.

마우스가 없는 터치 기기에서는 어떻게 동작하나요?

01·02·03·06처럼 :hover로 트리거되는 항목은 터치 기기에서 생략돼도 사진 자체는 이미 보이는 상태라 정보 손실이 없습니다. 반면 05·07·09는 클릭이나 탭으로 트리거되는 구조라 마우스가 없는 기기에서도 그대로 동작합니다.

zip 안의 vanilla와 react 코드가 다른가요?

동작은 같습니다. vanilla는 SCSS 변수를 파일 맨 위에 두고 실사용 경로와 캡처용 is-demo 경로를 함께 담고 있고, React 판은 is-demo 없이 실사용 경로만 컴포넌트 props로 받아 동일한 화면을 만듭니다. 정렬·확대·빛 상자까지 갖춘 갤러리 전체가 필요하면 이미지 갤러리 CSS 효과가 그 확장판입니다.

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

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