02Xám sang màu
filter:grayscale(1) rút màu ảnh, khi hover chuyển về 0 nên màu gốc dần hiện lại.
Dùng choẢnh hồ sơ giới thiệu team, card lọc trong gallery
filter:grayscaletransition0.4초

<main class="stage">
<figure class="photo is-demo" tabindex="0">
<span class="photo__pic" aria-hidden="true">
<span class="photo__sun"></span>
<span class="photo__hill"></span>
</span>
<figcaption class="photo__cap">Khu vườn mùa xuân</figcaption>
</figure>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-base; // 채도가 돌아오는 시간
$easing: $ease-out; // 시작 빠르고 끝은 부드럽게
$color: $subject-ink; // 사진 프레임 색
@include stage($stage-yellow);
.stage { padding: clamp(6px, 2.4vw, 16px); } // 320px 대응: 데모 카드가 작아 여백도 줄인다
.photo {
margin: 0;
width: min(160px, 50vw);
border-radius: $r-card;
overflow: hidden;
box-shadow: $shadow-raised;
border: 3px solid $color;
outline: none;
cursor: pointer;
}
.photo__pic {
display: block;
position: relative;
aspect-ratio: 4 / 3;
background: linear-gradient(155deg, $subject-cream 0%, $subject-blue 100%);
filter: grayscale(1);
transition: filter $duration $easing;
}
.photo__sun {
position: absolute;
top: 16%;
right: 18%;
width: 20%;
aspect-ratio: 1;
border-radius: 50%;
background: $stage-orange;
}
.photo__hill {
position: absolute;
left: -8%;
right: -8%;
bottom: -10%;
height: 48%;
border-radius: 50% 50% 0 0 / 100% 100% 0 0;
background: $subject-ink;
opacity: .85;
}
.photo__cap {
word-break: keep-all;
margin: 0;
padding: 10px 14px;
font-weight: 600; font-size: clamp(12px, 3.4vw, 14px); line-height: 1.3; font-family: inherit;
color: $subject-cream;
background: $color;
}
.photo:hover .photo__pic,
.photo:focus-visible .photo__pic,
.photo:active .photo__pic {
filter: grayscale(0);
}
.photo.is-demo .photo__pic {
animation: color-loop $dur-loop $easing infinite;
}
@keyframes color-loop {
0%, 20% { filter: grayscale(1); }
50%, 75% { filter: grayscale(0); }
100% { filter: grayscale(1); }
}
@media (hover: none) {
.photo:active .photo__pic { filter: grayscale(0); }
}
@media (prefers-reduced-motion: reduce) {
.photo__pic { transition: none; }
.photo.is-demo .photo__pic { animation: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffd23f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.stage {
padding: clamp(6px, 2.4vw, 16px);
}
.photo {
margin: 0;
width: min(160px, 50vw);
border-radius: 16px;
overflow: hidden;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
border: 3px solid #17141a;
outline: none;
cursor: pointer;
}
.photo__pic {
display: block;
position: relative;
aspect-ratio: 4/3;
background: linear-gradient(155deg, #fff7e6 0%, #2f6df6 100%);
filter: grayscale(1);
transition: filter 300ms ease-out;
}
.photo__sun {
position: absolute;
top: 16%;
right: 18%;
width: 20%;
aspect-ratio: 1;
border-radius: 50%;
background: #ff4d1f;
}
.photo__hill {
position: absolute;
left: -8%;
right: -8%;
bottom: -10%;
height: 48%;
border-radius: 50% 50% 0 0/100% 100% 0 0;
background: #17141a;
opacity: 0.85;
}
.photo__cap {
word-break: keep-all;
margin: 0;
padding: 10px 14px;
font-weight: 600;
font-size: clamp(12px, 3.4vw, 14px);
line-height: 1.3;
font-family: inherit;
color: #fff7e6;
background: #17141a;
}
.photo:hover .photo__pic,
.photo:focus-visible .photo__pic,
.photo:active .photo__pic {
filter: grayscale(0);
}
.photo.is-demo .photo__pic {
animation: color-loop 2s ease-out infinite;
}
@keyframes color-loop {
0%, 20% {
filter: grayscale(1);
}
50%, 75% {
filter: grayscale(0);
}
100% {
filter: grayscale(1);
}
}
@media (hover: none) {
.photo:active .photo__pic {
filter: grayscale(0);
}
}
@media (prefers-reduced-motion: reduce) {
.photo__pic {
transition: none;
}
.photo.is-demo .photo__pic {
animation: none;
}
}import type { CSSProperties } from "react";
import styles from "./GrayscaleToColorPhoto.module.scss";
export type GrayscaleToColorPhotoProps = {
caption: string;
/** 채도가 돌아오는 시간 (기본 300ms) */
duration?: number;
easing?: string;
/** 프레임 색 */
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function GrayscaleToColorPhoto({
caption,
duration = 300,
easing = "ease-out",
color = "#17141a",
}: GrayscaleToColorPhotoProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<figure className={styles.photo} style={vars} tabIndex={0}>
<span className={styles.pic} aria-hidden="true">
<span className={styles.sun} />
<span className={styles.hill} />
</span>
<figcaption className={styles.cap}>{caption}</figcaption>
</figure>
);
}02 Xám sang màu — Diện tích đo được 12.272% · khung hình chuyển động 10/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · 9 CSS Image Hover Effects — dán 1 dòng là chạy
Bài viết chứa demo này
9 CSS Image Hover Effects — dán 1 dòng là chạyCSS 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. Chín kiểu Xem tất cả demo trong bài này


