A Guide to CSS filter (Grayscale, Blur, and More)

CSS filter is a powerful, underused tool that lets you style pixels at render time. You can grayscale a photo for a subtle UI, blur thumbnails to hide spoilers, and hue-rotate a card to match a theme, no extra assets, no image editor. In this guide you will build practical utilities and components that showcase grayscale, blur, contrast, and more, so you can drop them straight into real projects.

Why CSS filter Matters

Filters give you design control without a trip to Photoshop. They are fast to iterate, easy to theme with CSS variables, and trivial to toggle on interaction. You can keep one source image and render it in multiple moods across a site. Filters also stack, so you can mix grayscale with contrast tweaks, or add a crisp drop-shadow that respects transparency. If you already build UI blocks and CSS shapes, filters extend those blocks with polish, think a muted avatar that colorizes on hover, or a subtle blur on a hero image behind a headline.

Prerequisites

You only need a working knowledge of HTML and CSS. We will use custom properties and a few pseudo-elements for small accents.

  • Basic HTML
  • CSS custom properties
  • CSS pseudo-elements (::before / ::after)

Step 1: The HTML Structure

The markup includes a hero with a background image, a gallery of cards showing different filter recipes, and a small utility demo grid. Each figure wraps an image and a caption so you can test hover states and transitions. One card includes a small directional badge made with a triangle to point attention to the caption.

<!-- HTML -->
<header class="hero">
  <img class="hero__bg" src="https://images.unsplash.com/photo-1501785888041-af3ef285b470?w=1600" alt="Pine forest at sunrise">
  <h1>CSS Filter Gallery</h1>
</header>

<main class="page">
  <section class="gallery">
    <figure class="card card--grayscale">
      <img src="https://images.unsplash.com/photo-1488521787991-ed7bbaae773c?w=800" alt="Mountain lake">
      <figcaption>Grayscale on hover (returns to color)</figcaption>
    </figure>

    <figure class="card card--blur">
      <img src="https://images.unsplash.com/photo-1469474968028-56623f02e42e?w=800" alt="Desert road">
      <figcaption>Blurred by default, sharp on hover</figcaption>
    </figure>

    <figure class="card card--duotone">
      <img src="https://images.unsplash.com/photo-1469474968028-56623f02e42e?w=800" alt="Desert road (duotone)">
      <figcaption>Duotone with hue-rotate + saturate</figcaption>
    </figure>

    <figure class="card card--shadow-badge">
      <img src="https://images.unsplash.com/photo-1491553895911-0055eca6402d?w=800" alt="City skyline">
      <figcaption>Drop-shadow on transparent PNG + directional badge</figcaption>
    </figure>
  </section>

  <section class="utilities-demo">
    <h2 class="utilities-demo__title">Filter Utility Sampler</h2>
    <div class="util-grid">
      <img class="u-grayscale" src="https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=400" alt="Forest">
      <img class="u-contrast-125" src="https://images.unsplash.com/photo-1507525428034-b723cf961d3e?w=400" alt="Ocean">
      <img class="u-brightness-75" src="https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?w=400" alt="City night">
      <img class="u-sepia" src="https://images.unsplash.com/photo-1469474968028-56623f02e42e?w=400" alt="Road">
      <img class="u-hue-rotate-90 u-saturate-200" src="https://images.unsplash.com/photo-1469474968028-56623f02e42e?w=400" alt="Road hued">
      <img class="u-invert" src="https://images.unsplash.com/photo-1517816428104-797678c7cf0d?w=400" alt="Desert dunes">
    </div>
  </section>
</main>

Step 2: The Basic CSS & Styling

Start with a light system for spacing and colors, a responsive grid for the gallery, and some defaults for images. Custom properties give you quick hooks to tune filter intensity later without changing multiple rules.

/* CSS */
:root {
  --space-1: .5rem;
  --space-2: 1rem;
  --space-3: 1.5rem;
  --radius: 12px;
  --bg: #0f1115;
  --panel: #171a21;
  --text: #e6e6e6;
  --muted: #9aa0a6;

  /* Filter intensity tokens */
  --blur-sm: 2px;
  --blur-md: 6px;
  --blur-lg: 12px;

  --contrast-125: 1.25;
  --brightness-75: .75;
  --saturate-200: 2;
}

*,
*::before,
*::after { box-sizing: border-box; }

html, body {
  height: 100%;
  background: var(--bg);
  color: var(--text);
  font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji";
}

img { display: block; max-width: 100%; height: auto; }

.hero {
  position: relative;
  min-height: 40vh;
  display: grid;
  place-items: center;
  overflow: hidden;
  border-bottom: 1px solid #202431;
}

.hero__bg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: brightness(.6) saturate(1.2) blur(var(--blur-sm));
  transform: scale(1.05);
}

.hero h1 {
  position: relative;
  margin: 0;
  padding: var(--space-2) var(--space-3);
  backdrop-filter: blur(2px);
  background: color-mix(in oklab, #000 35%, transparent);
  border-radius: var(--radius);
  font-size: clamp(1.5rem, 2vw + 1rem, 2.5rem);
}

.page { padding: var(--space-3); }

.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: var(--space-3);
  margin-block: var(--space-3);
}

.card {
  background: var(--panel);
  border: 1px solid #212636;
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: 0 1px 0 rgba(0,0,0,.25), 0 12px 32px rgba(0,0,0,.25);
}

.card figcaption {
  color: var(--muted);
  font-size: .9rem;
  padding: var(--space-2);
  border-top: 1px solid #202431;
}

.utilities-demo__title {
  margin-block: var(--space-3) var(--space-2);
  font-size: 1.25rem;
}

.util-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: var(--space-2);
}

.util-grid img {
  border-radius: 10px;
  border: 1px solid #212636;
}

Advanced Tip: Keep filter intensity in custom properties (like –blur-md or –contrast-125). You can theme a whole section by overriding the variables on a parent, without touching the filter functions in each rule.

Step 3: Building Filter Utilities

Utilities let you compose effects quickly. You will create single-purpose classes for grayscale, blur, contrast, brightness, saturation, sepia, hue-rotate, invert, and drop-shadow. You can stack them in markup for compound effects.

/* CSS */
.u-grayscale { filter: grayscale(100%); }
.u-contrast-125 { filter: contrast(var(--contrast-125)); }
.u-brightness-75 { filter: brightness(var(--brightness-75)); }
.u-saturate-200 { filter: saturate(var(--saturate-200)); }
.u-sepia { filter: sepia(100%); }
.u-hue-rotate-90 { filter: hue-rotate(90deg); }
.u-invert { filter: invert(100%); }

.u-blur-sm { filter: blur(var(--blur-sm)); }
.u-blur-md { filter: blur(var(--blur-md)); }
.u-blur-lg { filter: blur(var(--blur-lg)); }

/* Compose by stacking filters on the same rule if needed */
.u-hue-rotate-90.u-saturate-200 { filter: hue-rotate(90deg) saturate(var(--saturate-200)); }

/* A subtle drop-shadow that respects transparency */
.u-drop-shadow {
  /* drop-shadow offsets X, Y, blur, color */
  filter: drop-shadow(0 6px 10px rgba(0,0,0,.35));
}

How This Works (Code Breakdown)

Each utility sets the filter property with one or more functions. Percent-based functions such as grayscale(), sepia(), and invert() accept 0% to 100%. Numeric functions such as contrast() and saturate() use multipliers, where 1 is unchanged, values above 1 increase the effect, and values between 0 and 1 reduce it. hue-rotate() takes an angle; 90deg shifts colors by a quarter turn on the color wheel.

blur() accepts a radius in length units. Smaller radii like 2px give a soft defocus suitable for backgrounds. Larger values quickly become expensive on large images. Keep an eye on the painted size of the element; a full-bleed hero with blur(20px) costs more than a small card with blur(6px).

drop-shadow() is not the same as box-shadow. box-shadow traces the element box, while drop-shadow follows the alpha channel of the rendered pixels. Use it for transparent PNGs, CSS icons, or shapes to get a crisp shadow around the silhouette. If you are building icons from shapes such as a circular avatar, you can combine a shape with a shadow. For a refresher on making a circle, see how to make a circle with CSS.

Utilities can be combined by applying multiple classes. When you need both hue-rotation and extra saturation, apply both classes or write a combined rule. Filter functions run left to right, so the order changes the result. For instance, grayscale(100%) followed by hue-rotate() still yields gray pixels, because the color was removed first. Flip the order and you will get a very different look.

Step 4: Building Interactive Cards

Next, wire up the gallery cards to showcase practical interactions: grayscale that reveals color on hover, blur that sharpens on hover, a duotone treatment using hue-rotate and saturate, and a drop-shadow on a transparent image. One card adds a small badge using a right-pointing triangle to nudge attention toward the caption.

/* CSS */
/* 1) Grayscale to color on hover */
.card--grayscale img {
  filter: grayscale(100%) contrast(1.05);
  transition: filter .35s ease;
}
.card--grayscale:hover img,
.card--grayscale:focus-within img {
  filter: none;
}

/* 2) Blur reveal on hover */
.card--blur img {
  filter: blur(var(--blur-md)) brightness(.9);
  transform: scale(1.03);
  transition: filter .35s ease, transform .35s ease;
}
.card--blur:hover img,
.card--blur:focus-within img {
  filter: blur(0);
  transform: scale(1);
}

/* 3) Duotone look: grayscale + sepia + hue shifts + heavy saturate */
.card--duotone img {
  filter:
    grayscale(100%)
    sepia(60%)
    contrast(1.1)
    brightness(.9)
    hue-rotate(260deg)
    saturate(5);
  transition: filter .35s ease;
}
.card--duotone:hover img { filter: hue-rotate(220deg) saturate(5.5) contrast(1.15) brightness(.95); }

/* 4) Shadow and badge */
.card--shadow-badge {
  position: relative;
}
.card--shadow-badge img {
  /* Requires image with transparency for best effect */
  filter: drop-shadow(0 10px 16px rgba(0,0,0,.4));
  transition: filter .35s ease;
}
.card--shadow-badge:hover img { filter: drop-shadow(0 16px 24px rgba(0,0,0,.5)); }

/* A small directional badge using a triangle */
.card--shadow-badge figcaption {
  position: relative;
  padding-left: calc(var(--space-2) + 10px);
}
.card--shadow-badge figcaption::before {
  content: "";
  position: absolute;
  left: var(--space-2);
  top: 50%;
  transform: translateY(-50%);
  width: 0; height: 0;
  border-top: 6px solid transparent;
  border-bottom: 6px solid transparent;
  border-left: 10px solid #64b5f6; /* right-pointing triangle */
  filter: drop-shadow(0 2px 3px rgba(0,0,0,.35));
}

How This Works (Code Breakdown)

The grayscale card starts fully desaturated, then returns to normal on hover. The contrast bump keeps midtones crisp while gray. The blur card scales the image up slightly to reduce edge artifacts; when the blur radius drops to zero on hover, the transform returns to 1 to avoid a pop.

The duotone recipe relies on a chain: remove color with grayscale, add warmth with sepia, then push hues into a new range with hue-rotate while increasing saturation to create a strong palette. Small tweaks to brightness and contrast keep the image balanced. On hover, a slight hue shift adds motion without moving layout.

The shadow badge card demonstrates drop-shadow on transparency and a pointer built from borders. The triangle is made by collapsing width and height to zero and drawing borders on two sides transparent while filling one side with color. If you want a deeper dive into triangle techniques, see how to make a right-pointing triangle with CSS. You can adapt the same pointer to tooltips or banners, which pairs well with filter-driven emphasis.

Advanced Techniques: Animations & Hover Effects

Filters animate well for small deltas. Hue rotation delivers rich transitions, and subtle blur fades can add depth. Keep the animated area small and the frame budget safe.

/* CSS */
/* Animated hue loop for attention */
@keyframes hue-cycle {
  0% { filter: hue-rotate(0deg) saturate(1.5); }
  50% { filter: hue-rotate(180deg) saturate(2); }
  100% { filter: hue-rotate(360deg) saturate(1.5); }
}

.card--duotone:hover img {
  animation: hue-cycle 2.5s linear infinite;
}

/* Reveal text with background blur using backdrop-filter on the caption */
.card figcaption {
  backdrop-filter: blur(3px);
  background: color-mix(in oklab, #0b0e14 55%, transparent);
  transition: backdrop-filter .3s ease, background-color .3s ease;
}

/* Smooth grayscale ramp using steps to reduce repaint toggles */
@keyframes degray {
  0% { filter: grayscale(100%); }
  100% { filter: grayscale(0%); }
}
.card--grayscale:hover img {
  animation: degray .45s ease-out forwards;
}

Note: backdrop-filter blurs what is behind the element, not its own pixels. Keep the blurred area small (like a caption bar) for better performance. The hero title earlier used a tiny backdrop blur to improve legibility without heavy cost.

Accessibility & Performance

Filters alter color and clarity, which can affect readability and recognition. Treat them as decoration around content, not as a gate to content. Keep strong treatments opt-in and reversible on interaction.

Accessibility

Provide descriptive alt text for images. If a filtered thumbnail is purely decorative, mark it with an empty alt attribute so assistive tech skips it. If an effect signals state, make sure the same state is conveyed in text, ARIA, or both. For example, a desaturated card that means “disabled” should also include aria-disabled=”true” on a relevant control.

Respect motion preferences. Animated hue-rotations and blur transitions can distract. Wrap your animation rules with a prefers-reduced-motion query and fall back to static filters.

/* CSS */
@media (prefers-reduced-motion: reduce) {
  .card img, .u-hue-rotate-90, .u-blur-sm, .u-blur-md, .u-blur-lg {
    animation: none !important;
    transition: none !important;
  }
}

Be careful with blur over text. If you blur a container with text inside, the glyphs blur too, which harms legibility. Prefer backdrop-filter on a separate overlay behind text, or filter the image element only.

Performance

Filters run in the compositor on most modern browsers. They still carry a cost that scales with pixel area. blur() and drop-shadow() are the heaviest because they sample neighboring pixels. Use smaller radii and confine effects to smaller elements. Transitioning filter values is also more costly than animating transform or opacity, so limit duration and frequency. When you need a strong, consistent silhouette shadow on a CSS-built icon (for instance, a circular avatar or a geometric logo), drop-shadow beats box-shadow for fidelity. If you want to build that avatar from CSS, this guide on how to make a circle with CSS pairs nicely with a subtle filter on hover.

For pointer accents and callouts, you can craft triangles and apply a small drop-shadow for depth. Reference the triangle primer here: how to make a right-pointing triangle with CSS. Reuse the same trick in tooltips and ribbons across your design system.

Ship Production-Ready Filter Recipes

You now have a compact set of utilities and four interaction patterns powered by CSS filter: grayscale reveals, blur reveals, duotone tints, and alpha-aware shadows. These techniques layer cleanly onto existing components and CSS shapes with minimal markup. Keep honing your own recipes, and you will have a library of photo treatments ready for any new layout or theme.

Leave a Comment