30 CSS Hover Effects 21 / 30

CSS Duotone Image Hover Effect

Four duotone image hover transitions — grayscale to full color restore, monochrome to brand duotone, hue rotation shift, and saturation fade-in — using CSS filter chains and pseudo-element blending to apply and remove two-tone color grades on hover.

Pure CSS MIT licensed
Live Demo Open in tab

This is a full-page demo — interact inside the frame above, or open it in the playground for the full-screen experience.

Open in playground

The code

<div class="hv-21">
  <div class="hv-21__grid">
    <div class="hv-21__cell">
      <div class="hv-21__wrap hv-21__wrap--restore">
        <div class="hv-21__img hv-21__swatch--a"></div>
      </div>
      <span class="hv-21__label">grayscale → color restore</span>
    </div>
    <div class="hv-21__cell">
      <div class="hv-21__wrap hv-21__wrap--duo">
        <div class="hv-21__img hv-21__swatch--b"></div>
        <div class="hv-21__duo-overlay"></div>
      </div>
      <span class="hv-21__label">duotone → natural</span>
    </div>
    <div class="hv-21__cell">
      <div class="hv-21__wrap hv-21__wrap--hue">
        <div class="hv-21__img hv-21__swatch--c"></div>
      </div>
      <span class="hv-21__label">hue-rotate grade</span>
    </div>
    <div class="hv-21__cell">
      <div class="hv-21__wrap hv-21__wrap--sat">
        <div class="hv-21__img hv-21__swatch--d"></div>
      </div>
      <span class="hv-21__label">desaturate fade-in</span>
    </div>
  </div>
</div>
.hv-21,.hv-21 *,.hv-21 *::before,.hv-21 *::after{box-sizing:border-box;margin:0;padding:0}
.hv-21 ::selection{background:#9333ea;color:#fff}
.hv-21{
  --bg:#080608;
  --dim:#6b7280;
  font-family:'Segoe UI',system-ui,sans-serif;
  background:var(--bg);
  min-height:100vh;
  display:flex;align-items:center;justify-content:center;
  padding:60px 24px;
}
.hv-21__grid{
  display:grid;grid-template-columns:repeat(2,1fr);gap:32px;
  max-width:720px;width:100%;
}
.hv-21__cell{display:flex;flex-direction:column;gap:12px}
.hv-21__label{font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--dim)}
.hv-21__wrap{
  aspect-ratio:16/9;border-radius:12px;
  overflow:hidden;cursor:pointer;position:relative;
  isolation:isolate;
}
.hv-21__img{width:100%;height:100%}

/* layered pseudo-images so duotone/grayscale/hue-rotate are visually meaningful */
.hv-21__swatch--a{
  background:
    radial-gradient(circle at 28% 32%,#fef9c3 0%,transparent 14%),
    radial-gradient(circle at 70% 65%,#dc2626 0%,transparent 22%),
    repeating-linear-gradient(45deg,transparent 0 16px,rgba(255,255,255,.08) 16px 17px),
    repeating-linear-gradient(-45deg,transparent 0 24px,rgba(0,0,0,.14) 24px 25px),
    linear-gradient(135deg,#1e3a5f,#2563eb,#7dd3fc,#1e40af);
}
.hv-21__swatch--b{
  background:
    radial-gradient(circle at 32% 30%,#fde047 0%,transparent 16%),
    radial-gradient(circle at 68% 68%,#10b981 0%,transparent 22%),
    repeating-linear-gradient(30deg,transparent 0 18px,rgba(255,255,255,.1) 18px 19px),
    repeating-linear-gradient(-60deg,transparent 0 28px,rgba(0,0,0,.14) 28px 29px),
    linear-gradient(135deg,#4a1942,#7c3aed,#ddd6fe,#6d28d9);
}
.hv-21__swatch--c{
  background:
    radial-gradient(circle at 25% 30%,#fef3c7 0%,transparent 15%),
    radial-gradient(circle at 72% 70%,#dc2626 0%,transparent 22%),
    repeating-linear-gradient(60deg,transparent 0 20px,rgba(255,255,255,.1) 20px 21px),
    repeating-linear-gradient(-30deg,transparent 0 30px,rgba(0,0,0,.14) 30px 31px),
    linear-gradient(135deg,#14532d,#16a34a,#bbf7d0,#15803d);
}
.hv-21__swatch--d{
  background:
    radial-gradient(circle at 30% 35%,#fef9c3 0%,transparent 14%),
    radial-gradient(circle at 70% 68%,#1e40af 0%,transparent 22%),
    repeating-linear-gradient(75deg,transparent 0 18px,rgba(255,255,255,.1) 18px 19px),
    repeating-linear-gradient(-15deg,transparent 0 28px,rgba(0,0,0,.14) 28px 29px),
    linear-gradient(135deg,#7c2d12,#ea580c,#fdba74,#c2410c);
}

/* 1 — grayscale restore */
.hv-21__wrap--restore .hv-21__img{
  filter:grayscale(1) contrast(1.1);
  transition:filter .5s cubic-bezier(.4,0,.2,1);
}
.hv-21__wrap--restore:hover .hv-21__img{filter:none}

/* 2 — duotone overlay fade */
.hv-21__duo-overlay{
  position:absolute;inset:0;
  background:linear-gradient(135deg,#1e003c,#ff6b00);
  mix-blend-mode:color;
  opacity:1;
  transition:opacity .5s cubic-bezier(.4,0,.2,1);
}
.hv-21__wrap--duo .hv-21__img{filter:contrast(1.1)}
.hv-21__wrap--duo:hover .hv-21__duo-overlay{opacity:0}

/* 3 — hue rotate */
.hv-21__wrap--hue .hv-21__img{
  filter:hue-rotate(0deg) saturate(1);
  transition:filter .6s cubic-bezier(.4,0,.2,1);
}
.hv-21__wrap--hue:hover .hv-21__img{
  filter:hue-rotate(160deg) saturate(1.5);
}

/* 4 — desaturate reveal */
.hv-21__wrap--sat .hv-21__img{
  filter:grayscale(1);
  transition:filter .5s cubic-bezier(.4,0,.2,1);
}
.hv-21__wrap--sat:hover .hv-21__img{
  filter:grayscale(0) saturate(1.2);
}

@media(max-width:520px){.hv-21__grid{grid-template-columns:1fr}}
@media(prefers-reduced-motion:reduce){
  .hv-21__img,.hv-21__duo-overlay{transition:none!important}
}

How this works

CSS duotone simulation uses a filter: grayscale(1) on the image plus a ::after pseudo-element with a two-stop gradient and mix-blend-mode: color — the blend mode maps the gradient colors onto the image's luminance values without affecting lightness, producing a true duotone. On hover, removing or fading the blend overlay restores natural color.

The hue-rotate shift variant applies filter: hue-rotate(0deg) at rest and transitions to filter: hue-rotate(var(--hue)) on hover — since hue-rotate acts uniformly on all colors, the entire image shifts hue simultaneously. Combining it with saturate(1.4) intensifies the shift for a more graphic result. The saturation fade starts at filter: grayscale(1) contrast(1.1) and transitions to filter: none on hover for a classic desaturated-to-color reveal.

Customize

  • Swap duotone colors by editing the two stops in linear-gradient(var(--shadow-color), var(--highlight-color)) — try #000 to #ff6600 for an amber monotone.
  • Control the transition speed with transition-duration — a slow 1s fade between grayscale and color feels editorial; a fast .2s feels interactive.
  • Use mix-blend-mode: hue instead of color on the gradient overlay to preserve the image's original saturation levels while only shifting hue.
  • Stack two pseudo-elements with complementary blend modes for a more complex toning effect — the first with multiply and the second with screen.
  • Apply a subtle contrast(1.1) boost inside the grayscale state to ensure the image still reads clearly before the color reveal.

Watch out for

  • The mix-blend-mode: color duotone technique requires the image container to have isolation: isolate to prevent the blend mode from affecting elements outside the card.
  • filter: grayscale() is applied composited before mix-blend-mode — apply grayscale to the image img element, not the container that holds the blend overlay.
  • SVG-based duotone (via feColorMatrix) produces more accurate results but requires inline SVG — the CSS approach is a visual approximation sufficient for UI use cases.

Browser support

ChromeSafariFirefoxEdge
60+ 12+ 60+ 60+

filter chains and mix-blend-mode: color are fully supported in modern browsers.

Search CodeFronts

Loading…