Pure TailwindMIT licensed

Tailwind Grayscale to Color Image Hover

Three filter functions transitioned as one unit — and in utilities they are three named classes: grayscale brightness-75 saturate-0 at rest, group-hover:grayscale-0 group-hover:brightness-100 group-hover:saturate-120 on hover. The 20% saturation overshoot is what makes the colour land rather than merely appear.

Live Demo
Plain CSSTailwind v4
Try it

Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time

Untitled UI React · Affiliate link

Get Untitled UI Pro

The code

<section class="tih-06 w-full min-h-svh grid place-items-center bg-tih06-bg font-tih06-sans px-6 py-10" aria-label="Grayscale to colour image hover demo">
  <div class="grid w-full max-w-195 gap-3.5 grid-cols-2 sm:grid-cols-4">
    <article class="group relative overflow-hidden rounded-[10px] aspect-[3/4] ring-2 ring-transparent transition-[box-shadow] duration-550 ease-tih06 group-hover:ring-[oklch(0.79_0.13_205/0.4)] hover:ring-[oklch(0.79_0.13_205/0.4)] motion-reduce:transition-none">
      <div class="absolute inset-0 grid place-items-center bg-[image:url('https://i.pravatar.cc/400?img=47'),linear-gradient(135deg,oklch(0.3_0.05_250),oklch(0.4_0.17_265),oklch(0.62_0.19_260),oklch(0.82_0.09_250))] bg-cover bg-center bg-no-repeat grayscale brightness-75 saturate-0 transition-[filter,scale] duration-550 ease-tih06 group-hover:grayscale-0 group-hover:brightness-100 group-hover:saturate-120 group-hover:scale-104 motion-reduce:transition-none motion-reduce:grayscale-0 motion-reduce:brightness-100 motion-reduce:saturate-100 motion-reduce:group-hover:scale-100">
      </div>
      <div class="absolute inset-x-0 bottom-0 p-3 bg-linear-[to_top,oklch(0_0_0/0.8),transparent]">
        <p class="text-xs font-bold text-tih06-text">Aria Chen</p>
        <p class="mt-0.5 text-[10px] text-tih06-muted transition-colors duration-550 ease-tih06 group-hover:text-tih06-acc motion-reduce:transition-none">Lead Engineer</p>
      </div>
    </article>
    <article class="group relative overflow-hidden rounded-[10px] aspect-[3/4] ring-2 ring-transparent transition-[box-shadow] duration-550 ease-tih06 hover:ring-[oklch(0.79_0.13_205/0.4)] motion-reduce:transition-none">
      <div class="absolute inset-0 grid place-items-center bg-[image:url('https://i.pravatar.cc/400?img=12'),linear-gradient(135deg,oklch(0.25_0.08_45),oklch(0.45_0.15_45),oklch(0.63_0.22_28),oklch(0.8_0.1_25))] bg-cover bg-center bg-no-repeat grayscale brightness-75 saturate-0 transition-[filter,scale] duration-550 ease-tih06 group-hover:grayscale-0 group-hover:brightness-100 group-hover:saturate-120 group-hover:scale-104 motion-reduce:transition-none motion-reduce:grayscale-0 motion-reduce:brightness-100 motion-reduce:saturate-100 motion-reduce:group-hover:scale-100">
      </div>
      <div class="absolute inset-x-0 bottom-0 p-3 bg-linear-[to_top,oklch(0_0_0/0.8),transparent]">
        <p class="text-xs font-bold text-tih06-text">Marcus Webb</p>
        <p class="mt-0.5 text-[10px] text-tih06-muted transition-colors duration-550 ease-tih06 group-hover:text-tih06-acc motion-reduce:transition-none">Creative Director</p>
      </div>
    </article>
    <article class="group relative overflow-hidden rounded-[10px] aspect-[3/4] ring-2 ring-transparent transition-[box-shadow] duration-550 ease-tih06 hover:ring-[oklch(0.79_0.13_205/0.4)] motion-reduce:transition-none">
      <div class="absolute inset-0 grid place-items-center bg-[image:url('https://i.pravatar.cc/400?img=45'),linear-gradient(135deg,oklch(0.3_0.08_165),oklch(0.4_0.1_165),oklch(0.7_0.15_165),oklch(0.86_0.12_165))] bg-cover bg-center bg-no-repeat grayscale brightness-75 saturate-0 transition-[filter,scale] duration-550 ease-tih06 group-hover:grayscale-0 group-hover:brightness-100 group-hover:saturate-120 group-hover:scale-104 motion-reduce:transition-none motion-reduce:grayscale-0 motion-reduce:brightness-100 motion-reduce:saturate-100 motion-reduce:group-hover:scale-100">
      </div>
      <div class="absolute inset-x-0 bottom-0 p-3 bg-linear-[to_top,oklch(0_0_0/0.8),transparent]">
        <p class="text-xs font-bold text-tih06-text">Priya Nair</p>
        <p class="mt-0.5 text-[10px] text-tih06-muted transition-colors duration-550 ease-tih06 group-hover:text-tih06-acc motion-reduce:transition-none">Research Scientist</p>
      </div>
    </article>
    <article class="group relative overflow-hidden rounded-[10px] aspect-[3/4] ring-2 ring-transparent transition-[box-shadow] duration-550 ease-tih06 hover:ring-[oklch(0.79_0.13_205/0.4)] motion-reduce:transition-none">
      <div class="absolute inset-0 grid place-items-center bg-[image:url('https://i.pravatar.cc/400?img=33'),linear-gradient(135deg,oklch(0.25_0.13_305),oklch(0.45_0.22_300),oklch(0.63_0.21_300),oklch(0.89_0.06_305))] bg-cover bg-center bg-no-repeat grayscale brightness-75 saturate-0 transition-[filter,scale] duration-550 ease-tih06 group-hover:grayscale-0 group-hover:brightness-100 group-hover:saturate-120 group-hover:scale-104 motion-reduce:transition-none motion-reduce:grayscale-0 motion-reduce:brightness-100 motion-reduce:saturate-100 motion-reduce:group-hover:scale-100">
      </div>
      <div class="absolute inset-x-0 bottom-0 p-3 bg-linear-[to_top,oklch(0_0_0/0.8),transparent]">
        <p class="text-xs font-bold text-tih06-text">Leo Santos</p>
        <p class="mt-0.5 text-[10px] text-tih06-muted transition-colors duration-550 ease-tih06 group-hover:text-tih06-acc motion-reduce:transition-none">Product Strategist</p>
      </div>
    </article>
  </div>
</section>
@import "tailwindcss";

@theme {
  --color-tih06-bg: #09090f;
  --color-tih06-bg: oklch(0.13 0.008 285);
  --color-tih06-text: #f1f5f9;
  --color-tih06-text: oklch(0.96 0.005 250);
  --color-tih06-muted: #475569;
  --color-tih06-muted: oklch(0.63 0.03 255);
  --color-tih06-acc: #22d3ee;
  --color-tih06-acc: oklch(0.79 0.13 205);
  --font-tih06-sans: 'Space Grotesk', system-ui, sans-serif;
  --ease-tih06: cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
   w-full min-h-svh, so the stage is correctly sized before the browser build compiles. */

.tih-06 {
  width: 100%;
  min-height: 100svh;
}

.tih-06 ::selection {
  background: oklch(0.79 0.13 205);
  color: #000;
}

Setup — do this once per project

Skip this if your project already runs Tailwind v4.

1Load Tailwind
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>

Compiles in the browser — perfect for a CodePen or a quick test, not for production.

2Add the stylesheetRequiredthe @theme block is what makes the custom utilities exist
@import "tailwindcss";

@theme {
  --color-tih06-bg: #09090f;
  --color-tih06-bg: oklch(0.13 0.008 285);
  --color-tih06-text: #f1f5f9;
  --color-tih06-text: oklch(0.96 0.005 250);
  --color-tih06-muted: #475569;
  --color-tih06-muted: oklch(0.63 0.03 255);
  --color-tih06-acc: #22d3ee;
  --color-tih06-acc: oklch(0.79 0.13 205);
  --font-tih06-sans: 'Space Grotesk', system-ui, sans-serif;
  --ease-tih06: cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
   w-full min-h-svh, so the stage is correctly sized before the browser build compiles. */

.tih-06 {
  width: 100%;
  min-height: 100svh;
}

.tih-06 ::selection {
  background: oklch(0.79 0.13 205);
  color: #000;
}
3Fonts used by this demooptional — skip if you have your own type stack
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap">

What changed in the port

Which parts became utilities, which stayed CSS, and why.

  • A three-function filter declaration becomes three composable utilities, and the hovered stack is three more. This is the property where Tailwind's model is strictly better than hand-written CSS: partial states are token changes.
  • The ::after border pseudo-element disappears entirely, replaced by ring-2 ring-transparent plus a hover: ring colour — one fewer box, and no pointer-events:none to remember.
  • transition-[filter,scale] names both animating properties in one declaration, which is the documented workaround for the Firefox filter-plus-transform stutter the CSS version calls out.
  • The emoji's own grayscale is a second, independent filter on a child — trivial as a utility, a whole extra rule in CSS.
  • motion-reduce: lands the portraits in full colour immediately. The alternative (a slow fade) is exactly what the media query exists to prevent.
  • Nothing but the root rule and ::selection stays in the stylesheet; the border pseudo-element was the only CSS-only piece and utilities absorbed it.
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tailwind Grayscale to Color Image Hover
Source: https://codefronts.com/motion/tailwind-css-image-hover-effects/tailwind-grayscale-to-color-image-hover/

Three filter functions transitioned as one unit &mdash; and in utilities they are three named classes: grayscale brightness-75 saturate-0 at rest, group-hover:grayscale-0 group-hover:brightness-100 group-hover:saturate-120 on hover. The 20% saturation overshoot is what makes the colour land rather than merely appear.
## HTML
```html
<section class="tih-06 w-full min-h-svh grid place-items-center bg-tih06-bg font-tih06-sans px-6 py-10" aria-label="Grayscale to colour image hover demo">
  <div class="grid w-full max-w-195 gap-3.5 grid-cols-2 sm:grid-cols-4">
    <article class="group relative overflow-hidden rounded-[10px] aspect-[3/4] ring-2 ring-transparent transition-[box-shadow] duration-550 ease-tih06 group-hover:ring-[oklch(0.79_0.13_205/0.4)] hover:ring-[oklch(0.79_0.13_205/0.4)] motion-reduce:transition-none">
      <div class="absolute inset-0 grid place-items-center bg-[image:url('https://i.pravatar.cc/400?img=47'),linear-gradient(135deg,oklch(0.3_0.05_250),oklch(0.4_0.17_265),oklch(0.62_0.19_260),oklch(0.82_0.09_250))] bg-cover bg-center bg-no-repeat grayscale brightness-75 saturate-0 transition-[filter,scale] duration-550 ease-tih06 group-hover:grayscale-0 group-hover:brightness-100 group-hover:saturate-120 group-hover:scale-104 motion-reduce:transition-none motion-reduce:grayscale-0 motion-reduce:brightness-100 motion-reduce:saturate-100 motion-reduce:group-hover:scale-100">
      </div>
      <div class="absolute inset-x-0 bottom-0 p-3 bg-linear-[to_top,oklch(0_0_0/0.8),transparent]">
        <p class="text-xs font-bold text-tih06-text">Aria Chen</p>
        <p class="mt-0.5 text-[10px] text-tih06-muted transition-colors duration-550 ease-tih06 group-hover:text-tih06-acc motion-reduce:transition-none">Lead Engineer</p>
      </div>
    </article>
    <article class="group relative overflow-hidden rounded-[10px] aspect-[3/4] ring-2 ring-transparent transition-[box-shadow] duration-550 ease-tih06 hover:ring-[oklch(0.79_0.13_205/0.4)] motion-reduce:transition-none">
      <div class="absolute inset-0 grid place-items-center bg-[image:url('https://i.pravatar.cc/400?img=12'),linear-gradient(135deg,oklch(0.25_0.08_45),oklch(0.45_0.15_45),oklch(0.63_0.22_28),oklch(0.8_0.1_25))] bg-cover bg-center bg-no-repeat grayscale brightness-75 saturate-0 transition-[filter,scale] duration-550 ease-tih06 group-hover:grayscale-0 group-hover:brightness-100 group-hover:saturate-120 group-hover:scale-104 motion-reduce:transition-none motion-reduce:grayscale-0 motion-reduce:brightness-100 motion-reduce:saturate-100 motion-reduce:group-hover:scale-100">
      </div>
      <div class="absolute inset-x-0 bottom-0 p-3 bg-linear-[to_top,oklch(0_0_0/0.8),transparent]">
        <p class="text-xs font-bold text-tih06-text">Marcus Webb</p>
        <p class="mt-0.5 text-[10px] text-tih06-muted transition-colors duration-550 ease-tih06 group-hover:text-tih06-acc motion-reduce:transition-none">Creative Director</p>
      </div>
    </article>
    <article class="group relative overflow-hidden rounded-[10px] aspect-[3/4] ring-2 ring-transparent transition-[box-shadow] duration-550 ease-tih06 hover:ring-[oklch(0.79_0.13_205/0.4)] motion-reduce:transition-none">
      <div class="absolute inset-0 grid place-items-center bg-[image:url('https://i.pravatar.cc/400?img=45'),linear-gradient(135deg,oklch(0.3_0.08_165),oklch(0.4_0.1_165),oklch(0.7_0.15_165),oklch(0.86_0.12_165))] bg-cover bg-center bg-no-repeat grayscale brightness-75 saturate-0 transition-[filter,scale] duration-550 ease-tih06 group-hover:grayscale-0 group-hover:brightness-100 group-hover:saturate-120 group-hover:scale-104 motion-reduce:transition-none motion-reduce:grayscale-0 motion-reduce:brightness-100 motion-reduce:saturate-100 motion-reduce:group-hover:scale-100">
      </div>
      <div class="absolute inset-x-0 bottom-0 p-3 bg-linear-[to_top,oklch(0_0_0/0.8),transparent]">
        <p class="text-xs font-bold text-tih06-text">Priya Nair</p>
        <p class="mt-0.5 text-[10px] text-tih06-muted transition-colors duration-550 ease-tih06 group-hover:text-tih06-acc motion-reduce:transition-none">Research Scientist</p>
      </div>
    </article>
    <article class="group relative overflow-hidden rounded-[10px] aspect-[3/4] ring-2 ring-transparent transition-[box-shadow] duration-550 ease-tih06 hover:ring-[oklch(0.79_0.13_205/0.4)] motion-reduce:transition-none">
      <div class="absolute inset-0 grid place-items-center bg-[image:url('https://i.pravatar.cc/400?img=33'),linear-gradient(135deg,oklch(0.25_0.13_305),oklch(0.45_0.22_300),oklch(0.63_0.21_300),oklch(0.89_0.06_305))] bg-cover bg-center bg-no-repeat grayscale brightness-75 saturate-0 transition-[filter,scale] duration-550 ease-tih06 group-hover:grayscale-0 group-hover:brightness-100 group-hover:saturate-120 group-hover:scale-104 motion-reduce:transition-none motion-reduce:grayscale-0 motion-reduce:brightness-100 motion-reduce:saturate-100 motion-reduce:group-hover:scale-100">
      </div>
      <div class="absolute inset-x-0 bottom-0 p-3 bg-linear-[to_top,oklch(0_0_0/0.8),transparent]">
        <p class="text-xs font-bold text-tih06-text">Leo Santos</p>
        <p class="mt-0.5 text-[10px] text-tih06-muted transition-colors duration-550 ease-tih06 group-hover:text-tih06-acc motion-reduce:transition-none">Product Strategist</p>
      </div>
    </article>
  </div>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-tih06-bg: #09090f;
  --color-tih06-bg: oklch(0.13 0.008 285);
  --color-tih06-text: #f1f5f9;
  --color-tih06-text: oklch(0.96 0.005 250);
  --color-tih06-muted: #475569;
  --color-tih06-muted: oklch(0.63 0.03 255);
  --color-tih06-acc: #22d3ee;
  --color-tih06-acc: oklch(0.79 0.13 205);
  --font-tih06-sans: 'Space Grotesk', system-ui, sans-serif;
  --ease-tih06: cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
   w-full min-h-svh, so the stage is correctly sized before the browser build compiles. */

.tih-06 {
  width: 100%;
  min-height: 100svh;
}

.tih-06 ::selection {
  background: oklch(0.79 0.13 205);
  color: #000;
}
```

How this works

The filter stack is composable. This is the property Tailwind models best:

<div class="grayscale brightness-75 saturate-0
   transition-[filter,scale] duration-550 ease-tih06
   group-hover:grayscale-0 group-hover:brightness-100
   group-hover:saturate-120 group-hover:scale-104">

Each function is its own utility and Tailwind assembles the filter value, so a partially desaturated resting state is grayscale-60 — one token change, not a rewritten declaration.

The border pseudo-element is gone. The CSS version adds an ::after with a transparent border purely to animate a frame. ring-2 ring-transparent with a group-hover: colour does the same thing on the card itself, with no pseudo-element and no pointer-events: none caveat.

Stacking-context warning, still true. A non-none filter creates a stacking context, so the portrait layer is its own context on hover and z-order against unfiltered siblings can shift. The port keeps the info bar as a later sibling, above it in paint order regardless.

Make it yours

  • Partial resting colour: grayscale-60 instead of grayscale, per card if you like — it is a per-element utility.
  • Punch: group-hover:saturate-150 for editorial portraiture; saturate-105 for corporate.
  • Hold before fading back: delay-75 outside the group-hover: half applies only on mouse-out.
  • Frame weight: group-hover:ring-4 plus ring-offset-2 ring-offset-tih06-bg for a gapped frame.
  • Retheme through --color-tih06-acc, which paints the ring, the role line and the selection colour.

Gotchas — read before shipping

  • grayscale is the bare utility for 100%; grayscale-0 is the off switch. Writing grayscale-100 works but reads worse than the bare form.
  • A filter creates a stacking context. If a filtered card contains a fixed descendant, it will be clipped to the card — true in every browser, and not a Tailwind issue.
  • Name both properties in transition-[filter,scale]. Firefox drops a frame or two when a filter and a transform start on the same element from separate transition declarations.
  • Hover-only colour is decorative here; the name and role are always legible. Never let a filter carry meaning.
  • saturate-120 is not on Tailwind's default scale as a &ldquo;step&rdquo; — v4 accepts the bare number, which is why the overshoot needs no arbitrary value.
  • The browser CDN build compiles in the page and is for prototyping only. Ship the CLI or a plugin build.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

filter is unprefixed from Chrome 53, Safari 9.1 and Firefox 35; Tailwind emits no -webkit-filter, so add it manually if you must support Safari 9.

Techniques used in this demo

Search CodeFronts

Loading…