---
name: duotone-image-hover
description: Use when you want "Editorial, stylish, modern" - Applies two-color image treatment on hover.
---

# Duotone Image Hover

> **Category:** Image / Media  -  **Personality:** Editorial, stylish, modern
>
> **Best use:** Portfolios, blogs, campaigns
>
> **Ratings:** Professional 4/5 - Casual 3/5 - Exotic 3/5 - Visual impact 4/5 - Difficulty 2/5 - Performance cost 2/5

## What it is

A duotone maps a photo's full tonal range onto just **two colours** — one for the shadows, one for the highlights — the way a two-ink magazine print does. This effect shows each thumbnail in that stylised duotone at rest, then **fades the inks away on hover** so the image blooms back to full colour while a caption rises in. It reads as expensive editorial design (think a printed lookbook or a photographer's index page), which is why portfolios, blogs and campaign grids lean on it.

The demo builds it with **no SVG filter and no JavaScript**: the image is desaturated with `filter: grayscale()`, and two solid-colour layers blended with `mix-blend-mode: screen` and `multiply` recolour the shadows and highlights. Because those are plain CSS, the rest-to-colour transition animates smoothly — something a hard-swapped SVG `feColorMatrix` filter cannot do.

## Dependencies / CDN

**None - pure CSS.** No libraries, no `<canvas>`, no SVG required.

The demo only loads two display fonts (optional — swap for your own):

```html
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Archivo:wght@500;600;700&family=Fraunces:ital,opsz,wght@0,9..144,400;0,9..144,600;0,9..144,900;1,9..144,400;1,9..144,600&display=swap" rel="stylesheet">
```

## HTML

Each tile is a `<figure>`: one `<img>`, a caption, an index. The two duotone inks are CSS pseudo-elements, so there is **no extra markup** for the effect itself. `tabindex="0"` makes the tile keyboard- and tap-reachable so the reveal also fires on `:focus-within`.

```html
<div class="dt-grid">
  <figure class="dt-card dt-feat" tabindex="0">
    <img class="dt-img" src="portrait-01.jpg" alt="Studio portrait of a woman with a dark pixie cut">
    <span class="dt-idx">01</span>
    <figcaption class="dt-cap">
      <div class="dt-cap-title">Mara, in Monochrome</div>
      <div class="dt-cap-meta">Portrait &middot; Studio</div>
    </figcaption>
  </figure>

  <!-- three more .dt-card figures: dt-wide, dt-arch, dt-port -->
</div>
```

## CSS

```css
/* 1) THE FRAME — must establish its OWN blend context */
.dt-card{
  position:relative; isolation:isolate;   /* contains the blend to this tile only */
  overflow:hidden; border-radius:14px;
  --dt-shadow:#3a1f74;   /* screen ink   → recolours the shadows (deep violet) */
  --dt-light:#f6c79a;    /* multiply ink → recolours the highlights (apricot)  */
}

/* 2) THE IMAGE — desaturated at rest so the inks map onto clean luminance */
.dt-img{
  display:block; width:100%; height:100%; object-fit:cover; transform:scale(1.03);
  filter:grayscale(1) contrast(1.07) brightness(1.03);
  transition:filter .65s ease, transform .95s cubic-bezier(.2,.72,.2,1);
}

/* 3) THE TWO INKS — pseudo-children of the card, so they blend with the image below */
.dt-card::before,.dt-card::after{
  content:''; position:absolute; inset:0; z-index:2; pointer-events:none;
  transition:opacity .6s ease;
}
.dt-card::before{ background:var(--dt-shadow); mix-blend-mode:screen; }   /* black → violet  */
.dt-card::after { background:var(--dt-light);  mix-blend-mode:multiply; } /* white → apricot */

/* 4) HOVER / FOCUS — fade the inks out and restore full colour */
.dt-card:hover .dt-img,
.dt-card:focus-within .dt-img{ filter:grayscale(0) contrast(1) brightness(1); transform:scale(1.07); }
.dt-card:hover::before,        .dt-card:hover::after,
.dt-card:focus-within::before, .dt-card:focus-within::after{ opacity:0; }

/* 5) CAPTION — slides up on reveal (sits above the inks at z-index:5) */
.dt-cap{
  position:absolute; left:0; right:0; bottom:0; z-index:5; padding:30px 16px 14px;
  background:linear-gradient(to top,rgba(10,8,16,.9),rgba(10,8,16,.5) 52%,transparent);
  opacity:0; transform:translateY(15px);
  transition:opacity .45s ease, transform .55s cubic-bezier(.2,.72,.2,1);
}
.dt-card:hover .dt-cap,.dt-card:focus-within .dt-cap{ opacity:1; transform:translateY(0); }

/* editorial layout: tall feature + wide banner + two tiles */
.dt-grid{
  display:grid; gap:14px;
  grid-template-columns:1.12fr 1fr 1fr;
  grid-template-rows:repeat(2,clamp(152px,18.5vw,206px));
  grid-template-areas:"feat wide wide" "feat arch port";
}
.dt-feat{grid-area:feat} .dt-wide{grid-area:wide} .dt-arch{grid-area:arch} .dt-port{grid-area:port}

/* keep large motion out of reduced-motion; the ink fade stays (instant per global rule) */
@media (prefers-reduced-motion:reduce){
  .dt-card:hover,.dt-card:focus-within{ transform:none; }
  .dt-img,.dt-card:hover .dt-img,.dt-card:focus-within .dt-img{ transform:scale(1.03); }
  .dt-cap{ transform:none; }
}
```

## JavaScript

Not required. The whole effect is `:hover` / `:focus-within` + `mix-blend-mode`. (`tabindex="0"` on each tile lets keyboard focus — and a tap on touch — trigger the same reveal with zero script.)

## How it works

- **Grayscale baseline.** `filter:grayscale(1)` flattens the photo to pure luminance (0 = black … 1 = white). The inks then have a clean tonal map to colour.
- **`mix-blend-mode:screen` with a dark colour** never darkens — it lifts the blacks up to the chosen colour. So shadows become **violet** while whites stay white.
- **`mix-blend-mode:multiply` with a light colour** never lightens — it pulls the whites down to that colour. So highlights become **apricot** while blacks stay black.
- Stacked, the two inks remap black → violet, white → apricot, mid-tones in between: a true two-colour duotone.
- **`isolation:isolate`** turns each card into its own blending group, so the inks mix with the image inside the tile and not with the page behind it.
- **The reveal** is just two transitions running together — the inks animate `opacity:1 → 0` while the image animates `grayscale(1) → 0`. The eye reads it as the picture "developing" from two inks into full colour.

## Customization

- **Re-ink it:** change the two custom properties — `--dt-shadow` (the `screen` colour) and `--dt-light` (the `multiply` colour). Teal + coral, ink-blue + cream, oxblood + gold all work. Keep the shadow colour darker than the highlight colour.
- **Per-tile palettes:** override `--dt-shadow`/`--dt-light` on an individual `.dt-card` for a mixed-ink contact sheet, or keep one palette for a cohesive "series".
- **Invert the logic:** start in colour and duotone on hover by flipping the `opacity` (inks `0` at rest, `1` on hover) and the `grayscale` values.
- **Punch / mood:** nudge the grayscale layer's `contrast()` and `brightness()` for a flatter or more dramatic curve before the inks land.
- **Pacing:** the `.6s`/`.65s` transitions give a slow "develop"; drop to `~.3s` for a snappier UI feel.
- **Alternative engine:** for a single-pass duotone you can swap the grayscale + two inks for an inline SVG `feColorMatrix`/`feComponentTransfer` filter — but it won't *transition* smoothly between duotone and colour, so the CSS-blend route is preferred for hover effects.

## Accessibility & performance

- **Alt text** describes the photograph, not the effect — the caption (`title` + `meta`) is supplementary.
- **Keyboard & touch:** `tabindex="0"` + `:focus-within` means Tab (and a tap on touch) reveals colour and caption exactly like hover. A visible `:focus-visible` outline is provided.
- **Reduced motion:** `@media (prefers-reduced-motion:reduce)` drops the lift and zoom; the colour change remains (and is shortened to near-instant by the catalog's global rule). The effect still works — just without travel.
- **Cheap:** `mix-blend-mode`, `grayscale`, and `opacity` are GPU-composited. Animating `opacity`/`transform` (not the `filter` radius) keeps it smooth; a grid of a dozen tiles is fine.
- **Contrast:** the caption sits on a dark gradient scrim so its text stays legible over any photo.

## Gotchas

- **`isolation:isolate` is mandatory.** Without it the `mix-blend-mode` layers blend with whatever is *behind the card* (the page), not the image — you get a muddy tinted box instead of a duotone.
- **The ink layers must be children of the isolated element.** Here they are `::before`/`::after` of `.dt-card`. If you instead put them inside a separate `position:absolute; z-index` wrapper, that wrapper becomes its own stacking context and the inks blend against *nothing* — the effect silently disappears.
- **Don't swap the blend modes.** `screen` wants the dark/shadow colour and `multiply` wants the light/highlight colour; reversing them flattens or inverts the duotone.
- **SVG filters don't tween.** You can't CSS-transition a `filter:url(#duotone)` swap, which is exactly why this recipe uses grayscale + blended overlays for the animated reveal.
- **Touch has no hover.** Coarse-pointer devices rely on the focus/tap fallback above; if you need colour-by-default there, gate the duotone behind `@media (hover:hover)`.
- **`object-fit:cover` + `overflow:hidden`** are what let mixed portrait/landscape photos fill a fixed editorial grid without distortion.
