GODRICH

9 CSS Image Hover Effects — dán 1 dòng là chạy

CSS image hover effects là các hiệu ứng chỉ chạy bằng CSS khi con trỏ dừng trên một ảnh, không cần JavaScript.

Tự chạy · bấm ô để tới mục đó · cả 9 trong một zip

Chín hiệu ứng dưới đây xếp từ đơn giản nhất đến phức tạp nhất. Từ 01 đến 04 chỉ đổi một thuộc tính duy nhất — scale, filter, vị trí hoặc opacity — ngay trên :hover. 05 và 06 thêm chiều thứ hai: một góc nghiêng phối cảnh và một dải sáng di chuyển. 07 đến 09 là nhóm dạng ý tưởng: mở khung tròn bằng clip-path, đổi cảnh bằng hai ảnh chồng nhau, và một cú rung giật mình. Ở lưới phía trên, bạn xem cả chín hiệu ứng tự lặp lại bên trong một khung iframe cố định không cuộn được, vì khung đó không nhận được sự kiện con trỏ thật nên mỗi ô tự chuyển sang trạng thái hover của chính nó theo chu kỳ hai giây. Mọi tấm "ảnh" trong demo đều là một dải gradient CSS hai tông màu có hình mặt trời và quả đồi được vẽ chồng lên, không phải file ảnh thật, nên khi bạn thay bằng thẻ img hay ảnh nền thật thì toàn bộ rule vẫn chạy y nguyên. Cả chín hiệu ứng đều áp lại đúng trạng thái :hover cho cả :focus-visible và :active, và mỗi file còn có một fallback @media (hover: none) để khi thiết bị cảm ứng chạm vào cũng cho ra cùng kết quả.

01Phóng to (zoom in)

Overflow bị cắt trên wrapper trong khi chỉ ảnh bên trong phóng to qua transform ở :hover, nên khung giữ nguyên kích thước còn tấm ảnh trông như đang tiến gần lại. Hợp với thumbnail sản phẩm và ô portfolio, nơi viền khung và bóng đổ cần đứng yên trong khi chỉ ảnh chuyển động.

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);
}

@media (hover: none) {
  .photo:active .photo__pic { transform: scale(1.14); }
}

02Xám sang màu

Filter grayscale(1) rút hết màu ảnh lúc đứng yên, rồi chuyển về grayscale(0) khi hover, nên màu gốc dần hiện lại đúng theo thời lượng transition đã khai báo. Ảnh hồ sơ giới thiệu team hay card lọc trong gallery dùng cách này để đánh dấu ô nào đang được chú ý mà không cần thêm markup.

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);
}

@media (hover: none) {
  .photo:active .photo__pic { filter: grayscale(0); }
}

03Caption trượt lên

Thanh caption nằm ẩn hoàn toàn dưới đáy ảnh qua translateY(100%), rồi trượt về translateY(0) khi hover để lộ tiêu đề và mô tả chưa từng hiện ra trước đó. Danh sách card blog hay lưới kiểu tạp chí dùng cách này để caption không chiếm chỗ ảnh lúc bình thường.

translateYoverflow:hidden0.25초
.photo__cap {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  transform: translateY(100%);
  transition: transform $duration $easing;
}

.photo:hover .photo__cap,
.photo:focus-visible .photo__cap,
.photo:active .photo__cap {
  transform: translateY(0);
}

04Overlay mờ dần

Một lớp màu phủ kín ảnh đứng ở opacity 0 cho đến khi hover đẩy lên 1, rồi mờ dần trở lại ngay khi con trỏ rời đi. Ảnh nền hero và ảnh cover danh mục dùng đúng mẹo này để đặt một nhãn dễ đọc lên trên ảnh rối mắt, chỉ khi có người thật sự đang nhìn vào ô đó.

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;
}

05Nghiêng 3D

Bên trong parent mang thuộc tính perspective, chính tấm ảnh nghiêng theo rotateX và rotateY trong khi nhấc nhẹ theo trục Y, khiến card đọc như một vật thể thật chứ không phải hình chữ nhật phẳng. Card showcase sản phẩm hay ảnh chứng chỉ, huy hiệu là nơi hưởng lợi nhiều nhất, vì một khung vật lý trông đúng khi có chiều sâu ba chiều.

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);
}

06Quét sáng

Một dải linear-gradient nghiêng chéo nằm ngoài khung lúc đứng yên rồi dịch chuyển hết chiều rộng ảnh khi hover, mô phỏng một tia sáng quét qua bề mặt bóng đúng một lượt. Card sản phẩm cao cấp và ảnh nhấn mạnh huy hiệu dùng cú quét này như một điểm nhấn một lần chứ không phải trạng thái bật tắt.

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%);
  pointer-events: none;
}

.photo:hover .photo__sweep,
.photo:focus-visible .photo__sweep,
.photo:active .photo__sweep {
  transform: translateX(120%);
  transition: transform $duration $easing;
}

07Phóng to hình tròn clip-path

clip-path:circle() bắt đầu ở bán kính 0% và lớn dần tới 150% khi hover, để một ô cửa tròn mở rộng từ tâm ra cho tới khi phủ hết tấm ảnh nằm dưới một lớp nền tối hơn. Ảnh teaser sự kiện và card hiệu ứng spotlight dùng vòng tròn giãn rồi co này để giấu phần lớn bức ảnh cho tới khi có người thật sự dừng lại nhìn.

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%);
}

08Cross-fade hai ảnh

Hai tấm ảnh xếp chồng đúng cùng một vị trí bằng position:absolute, và tấm thứ hai chuyển từ opacity 0 sang 1 khi hover, thay hẳn cảnh đang hiện mà không tấm nào phải di chuyển. Ảnh sản phẩm before/after và bản xem trước tùy chọn màu cần cú đổi này giữ nguyên vị trí thay vì trượt, vì một phép so sánh có chuyển động khó nhìn hơn khi chỉ liếc mắt qua.

opacity 교차position:absolute0.4초
.photo__b {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity $duration $easing;
}

.photo:hover .photo__b,
.photo:focus-visible .photo__b,
.photo:active .photo__b {
  opacity: 1;
}

09Rung lắc

Một đoạn @keyframes ngắn đẩy ảnh sang trái rồi sang phải vài pixel theo từng bước không đều rồi dừng lại ở translateX(0) trước khi animation kết thúc. Nó hợp với ảnh sản phẩm gắn badge sale giới hạn hay bất kỳ ảnh banner nào cần cắt ngang ánh mắt đang lướt qua thay vì chỉ đứng yên cho đẹp.

@keyframestranslateX0.4초
.photo:hover .photo__pic,
.photo:focus-visible .photo__pic,
.photo:active .photo__pic {
  animation: shake $duration $easing;
}

@keyframes shake {
  0%, 100% { transform: translateX(0); }
  20%      { transform: translateX(-6px); }
  40%      { transform: translateX(5px); }
  60%      { transform: translateX(-4px); }
  80%      { transform: translateX(3px); }
}

Chỗ dễ hỏng ở đâu? — cái bẫy

Trong chín hiệu ứng này có một chỗ mượn nhầm từ vựng của một góc khác trong CSS, khiến nó không hoạt động được. Từ khoá farthest-corner đo khoảng cách tới góc xa nhất của khung, vốn thuộc cú pháp kích cỡ của radial-gradient(), nên rất dễ mang nguyên nó vào bên trong clip-path:circle() ở mục 07 khi bán kính 150% tràn hơi quá trên một card rất dẹt hoặc rất cao. Nhưng đối số bán kính của circle() chỉ nhận length, percentage, closest-side hoặc farthest-side — đúng bốn giá trị đó theo trang tham chiếu circle() của MDN, không giá trị nào chứa từ "corner" cả — nên khai báo clip-path: circle(farthest-corner at 50% 50%) đơn giản là không hợp lệ, và trình duyệt bỏ hẳn cả dòng đó thay vì làm tròn về giá trị gần đúng nhất. Vòng tròn đứng nguyên ở chỗ rule hợp lệ gần nhất để lại, mà lúc mới tải trang thì đó thường là 0%, tức là hiệu ứng không bao giờ mở ra được. Muốn khớp một tỉ lệ khung lệch thì hãy tăng thẳng con số phần trăm thay vì mượn từ khoá của gradient mà clip-path chưa bao giờ nhận, còn bộ đầy đủ với mọi fallback đã canh sẵn nằm trong file nén, mở bằng mật khẩu pbk89rn6 gõ đúng như nó hiện trên dòng này, không thêm khoảng trắng nào.

Khả năng tiếp cận

Cả chín hiệu ứng đều tôn trọng prefers-reduced-motion, nhưng không theo cùng một cách. Từ mục 01 đến 04, 06, 07 và 08 chỉ đưa thời lượng transition về none, nên trạng thái đã hover vẫn hiện ngay lập tức — ảnh đã phóng to, màu đã trở lại, caption đã lộ ra — chỉ là không còn quãng chuyển động dẫn tới đó. Mục 05 cũng làm y vậy với góc nghiêng, bật thẳng sang vị trí đã xoay thay vì trượt dần vào đó. Mục 09 là trường hợp khác hẳn: vì cú rung dựng hoàn toàn bằng animation dạng keyframes chứ không phải transition, reduced motion bỏ luôn animation đó ở cả hover, focus lẫn active, nên dưới thiết lập này tấm ảnh hoàn toàn không rung chứ không phải rung tức thì rồi dừng. Trang prefers-reduced-motion của MDN ghi rõ cách hệ điều hành bật media query này và mức hỗ trợ trình duyệt đi kèm.

Xem thêm các hiệu ứng CSS khác trong chuyên mục CSS, và trang giới thiệu nói rõ site này thực chất bán gì.

FAQ

Chỉ cần một hiệu ứng thôi, không lấy hết chín cái có được không?

Được — mỗi hiệu ứng đóng gói gọn trong khối .photo riêng, không chia sẻ trạng thái với mục khác, nên copy đúng phần SCSS của một mục cùng khung HTML đi kèm là đủ để chạy độc lập. Mục 01 (zoom) và mục 02 (grayscale) là hai bản đơn giản nhất nếu trang chỉ cần một chuyển động duy nhất.

Dùng chung với Tailwind hay một dự án React có ổn không?

Ổn. Demo biên dịch từ SCSS ra CSS thuần nên không phụ thuộc công cụ build nào cả, và một rule như transform: scale(1.14) map thẳng sang utility kiểu hover:scale-110 của Tailwind cho phần lớn chín mục. Riêng mục 09 dựng bằng @keyframes thay vì transition nên vẫn cần khai animation tùy chỉnh ngoài bộ utility mặc định, còn thư mục react/ trong zip có sẵn một component riêng cho mỗi hiệu ứng.

Zip có kèm css image hover effects generator để tự chỉnh không?

Không có generator — zip đưa thẳng chín file SCSS, bản CSS đã biên dịch và bản React tương ứng, chứ không phải một công cụ sinh biến thể theo yêu cầu. Muốn đổi mức scale hay tốc độ transition thì sửa trực tiếp các biến $duration, $easing, $color khai ở đầu mỗi file, thay vì chờ một form sinh lại rule.

Nhập mật khẩu file nén

Mật khẩu nằm ngay trong bài viết này. Đọc tới đó bạn sẽ thấy.