CSS Image Effects: 16 Filters, Blends and Masks

Most CSS image effects never touch the image file. The browser applies them while it draws the page, so one rule can turn a photo gray, round it off, or blur whatever sits behind it.

Below are 16 CSS image effects to try and copy, from filters and blend modes to masks, then a guide to each property.

By Bogdan Sandu · Updated October 28, 2026

CSS filter effects

The filter and backdrop-filter properties recolor, blur and shadow the pixels at render time. The file on the server never changes, so removing the rule brings the original photo back.

Grayscale to color link card

  • filter: grayscale()
  • :focus-visible
  • hover: none

The photo rests at grayscale(1) and fades to full color when the card is hovered or focused. Touch screens never fire hover, so a hover: none query shows the color version there by default.

View code
.ie-gray {
  display: block;
  width: min(100%, 340px);
  border-radius: 18px;
  overflow: clip;
  background: #fff;
  color: #18202b;
  text-decoration: none;
  box-shadow: 0 1px 2px rgba(16, 24, 40, .06), 0 14px 34px rgba(16, 24, 40, .12);
}
.ie-gray__media {
  display: block;
  aspect-ratio: 4 / 3;
  overflow: clip;
}
.ie-gray__media img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: grayscale(1) contrast(1.08);
  transition: filter .5s ease;
}
.ie-gray:hover .ie-gray__media img,
.ie-gray:focus-visible .ie-gray__media img {
  filter: grayscale(0) contrast(1);
}
.ie-gray__body {
  display: grid;
  gap: 4px;
  padding: 16px 18px 18px;
}
.ie-gray__title {
  font-family: "Fraunces", serif;
  font-size: 1.3rem;
  font-weight: 600;
  line-height: 1.2;
}
.ie-gray__meta {
  color: #556070;
  font-size: .9rem;
}
.ie-gray__more {
  margin-top: 8px;
  color: #1c6a57;
  font-size: .95rem;
  font-weight: 600;
}
.ie-gray:focus-visible {
  outline: 3px solid #1c6a57;
  outline-offset: 4px;
}
@media (hover: none) {
  .ie-gray__media img {
    filter: none;
  }
}
@media (prefers-reduced-motion: no-preference) {
  .ie-gray__media img {
    transition: filter .5s ease, transform .7s cubic-bezier(.2, .7, .2, 1);
  }
  .ie-gray:hover .ie-gray__media img,
  .ie-gray:focus-visible .ie-gray__media img {
    transform: scale(1.04);
  }
}

Photo editor filter presets

  • filter stacks
  • :has()
  • radio inputs
Old gabled houses with white window frames along a canal, small boats moored at the bank

Photo filter

Each preset is a chain of filter functions held in a custom property, and :has() on the checked radio swaps the chain. The functions run left to right, so sepia() before contrast() gives a different result than the reverse order.

View code
.ie-edit {
  --ie-f: none;
  --ie-code: "filter: none;";
  width: min(100%, 440px);
  padding: 14px;
  border-radius: 20px;
  background: #1b1d22;
  color: #eef0f4;
}
.ie-edit:has(input[value="vintage"]:checked) {
  --ie-f: sepia(.5) contrast(1.1) brightness(.95) saturate(.9);
  --ie-code: "filter: sepia(.5) contrast(1.1) brightness(.95) saturate(.9);";
}
.ie-edit:has(input[value="noir"]:checked) {
  --ie-f: grayscale(1) contrast(1.35) brightness(.9);
  --ie-code: "filter: grayscale(1) contrast(1.35) brightness(.9);";
}
.ie-edit:has(input[value="faded"]:checked) {
  --ie-f: contrast(.78) brightness(1.12) saturate(.65);
  --ie-code: "filter: contrast(.78) brightness(1.12) saturate(.65);";
}
.ie-edit:has(input[value="cool"]:checked) {
  --ie-f: hue-rotate(-14deg) saturate(1.25) brightness(1.03);
  --ie-code: "filter: hue-rotate(-14deg) saturate(1.25) brightness(1.03);";
}
.ie-edit__photo {
  aspect-ratio: 4 / 3;
  border-radius: 12px;
  overflow: clip;
}
.ie-edit__photo img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: var(--ie-f);
  transition: filter .35s ease;
}
.ie-edit__code {
  min-height: 2.8em;
  margin: 12px 2px;
  color: #a9ecc8;
  font: .78rem/1.4 ui-monospace, Consolas, monospace;
}
.ie-edit__code::before {
  content: var(--ie-code);
}
.ie-edit__presets {
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: 8px;
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
}
.ie-edit__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.ie-edit__preset {
  position: relative;
  display: grid;
  justify-items: center;
  gap: 6px;
  color: #c3c9d4;
  font-size: .78rem;
  cursor: pointer;
}
.ie-edit__preset input {
  position: absolute;
  inset: 0;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}
.ie-edit__preset img {
  display: block;
  width: min(100%, 56px);
  height: auto;
  aspect-ratio: 1;
  border-radius: 10px;
  object-fit: cover;
  outline: 2px solid transparent;
  outline-offset: 2px;
}
.ie-edit__preset--vintage img {
  filter: sepia(.5) contrast(1.1) brightness(.95) saturate(.9);
}
.ie-edit__preset--noir img {
  filter: grayscale(1) contrast(1.35) brightness(.9);
}
.ie-edit__preset--faded img {
  filter: contrast(.78) brightness(1.12) saturate(.65);
}
.ie-edit__preset--cool img {
  filter: hue-rotate(-14deg) saturate(1.25) brightness(1.03);
}
.ie-edit__preset input:checked ~ img {
  outline-color: #f4c84a;
}
.ie-edit__preset input:checked ~ span {
  color: #fff;
  font-weight: 600;
}
.ie-edit__preset input:focus-visible ~ img {
  outline: 3px solid #8fb8ff;
}

Blur-to-sharp lazy reveal

  • filter: blur()
  • img.complete
  • role="status"
A waterfall pouring into a mossy green gorge below pine-covered slopes
Image loaded

While the image loads, the frame shows its dominant color and the img sits at blur(20px). The load event removes the class and the photo sharpens into place. The class is only added by script, so without JavaScript the image simply shows sharp.

View code
.ie-lazy {
  width: min(100%, 420px);
  border-radius: 16px;
  overflow: clip;
  background: #fff;
  box-shadow: 0 12px 30px rgba(20, 32, 24, .12);
}
.ie-lazy__frame {
  aspect-ratio: 4 / 3;
  overflow: clip;
  background: #3f5c3b;
}
.ie-lazy__frame img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.ie-lazy.is-waiting .ie-lazy__frame img {
  filter: blur(20px);
  opacity: .55;
  transform: scale(1.1);
}
.ie-lazy__bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 10px 12px;
  padding: 12px 14px;
}
.ie-lazy__status {
  color: #46525c;
  font-size: .9rem;
}
.ie-lazy__replay {
  min-height: 44px;
  padding: 0 18px;
  border: 0;
  border-radius: 999px;
  background: #1e3a2f;
  color: #fff;
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}
.ie-lazy__replay:hover {
  background: #2b5243;
}
.ie-lazy__replay:focus-visible {
  outline: 3px solid #1e3a2f;
  outline-offset: 3px;
}
@media (prefers-reduced-motion: no-preference) {
  .ie-lazy__frame img {
    transition: filter .9s ease, opacity .9s ease, transform .9s ease;
  }
}

drop-shadow() versus box-shadow

  • filter: drop-shadow()
  • box-shadow
  • transparent SVG
Orange scalloped trail badge with a mountain shape cut out of its center
box-shadow traces the box
The same trail badge, its shadow following the scalloped outline
drop-shadow() traces the shape

The same transparent SVG badge with two shadows. box-shadow traces the rectangular box of the img, while drop-shadow() follows the alpha channel, so the shadow wraps the scalloped edge and even shows through the mountain cut-out.

View code
.ie-shadow {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 14px;
  width: min(100%, 520px);
}
.ie-shadow__cell {
  display: grid;
  justify-items: center;
  gap: 18px;
  margin: 0;
  padding: 26px 12px 16px;
  border-radius: 18px;
  background: #fbf3e6;
}
.ie-shadow__cell img {
  display: block;
  width: min(100%, 150px);
  height: auto;
}
.ie-shadow__box {
  box-shadow: 10px 12px 18px rgba(74, 42, 12, .35);
}
.ie-shadow__drop {
  filter: drop-shadow(10px 12px 8px rgba(74, 42, 12, .45));
}
.ie-shadow__cell figcaption {
  color: #5a4632;
  font-size: .85rem;
  line-height: 1.35;
  text-align: center;
}
.ie-shadow__cell code {
  display: block;
  color: #2f2216;
  font: 600 .82rem/1.4 ui-monospace, Consolas, monospace;
}

Frosted glass booking panel

  • backdrop-filter
  • @supports
  • translucent fill
A deep blue fjord winding between grey granite cliffs, hikers resting on the rocks in the foreground

Cliffside Cabins

3 nights · 2 guests · from €420

backdrop-filter blurs whatever sits behind the panel, not the panel itself, so the fill has to be translucent for the blur to show. The base style is a near-opaque white, and the @supports block lowers the opacity only where the blur actually works.

View code
.ie-frost {
  position: relative;
  width: min(100%, 460px);
  aspect-ratio: 4 / 3;
  border-radius: 20px;
  overflow: clip;
}
.ie-frost > img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.ie-frost__panel {
  position: absolute;
  right: 12px;
  bottom: 12px;
  left: 12px;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 2px 12px;
  padding: 12px 12px 12px 16px;
  border: 1px solid rgba(255, 255, 255, .6);
  border-radius: 14px;
  background: rgba(255, 255, 255, .9);
  color: #0e1b2c;
}
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
  .ie-frost__panel {
    background: rgba(255, 255, 255, .55);
    -webkit-backdrop-filter: blur(14px) saturate(1.4);
    backdrop-filter: blur(14px) saturate(1.4);
  }
}
.ie-frost__title {
  margin: 0;
  font-family: "Fraunces", serif;
  font-size: 1.15rem;
  font-weight: 600;
  line-height: 1.2;
}
.ie-frost__meta {
  grid-row: 2;
  margin: 0;
  color: #1f2b3a;
  font-size: .85rem;
}
.ie-frost__cta {
  grid-row: 1 / span 2;
  grid-column: 2;
  min-height: 44px;
  padding: 0 16px;
  border: 0;
  border-radius: 10px;
  background: #0e1b2c;
  color: #fff;
  font: inherit;
  font-size: .9rem;
  font-weight: 600;
  cursor: pointer;
}
.ie-frost__cta:hover {
  background: #1d3350;
}
.ie-frost__cta:focus-visible {
  outline: 3px solid #fff;
  outline-offset: 2px;
  box-shadow: 0 0 0 6px #0e1b2c;
}

Blend modes and image fills

Blend modes decide how a photo mixes with the color or layer underneath it. These demos use solid color layers only, plus one photo poured into the letters of a headline.

Duotone with two solid layers

  • mix-blend-mode
  • multiply + lighten
  • isolation
Aerial view of a downtown skyline at sunrise, a lake stretching to the horizon
Duotone palette

The frame is filled with the highlight color, the grayscale photo multiplies into it, and a solid ::after layer in the shadow color sits on top with lighten. Shadows land on the dark color and highlights on the light one. isolation: isolate keeps the blend from reaching the page behind.

View code
.ie-duo {
  --ie-dark: #13235b;
  --ie-light: #ff8f70;
  display: grid;
  gap: 12px;
  width: min(100%, 460px);
}
.ie-duo:has(input[value="mint"]:checked) {
  --ie-dark: #0f3b33;
  --ie-light: #a6f0c6;
}
.ie-duo:has(input[value="ink"]:checked) {
  --ie-dark: #2d0f45;
  --ie-light: #f9d348;
}
.ie-duo__frame {
  position: relative;
  aspect-ratio: 4 / 3;
  border-radius: 16px;
  overflow: clip;
  background: var(--ie-light);
  isolation: isolate;
}
.ie-duo__frame img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: grayscale(1) contrast(1.15);
  mix-blend-mode: multiply;
}
.ie-duo__frame::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--ie-dark);
  mix-blend-mode: lighten;
  pointer-events: none;
}
.ie-duo__picker {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
}
.ie-duo__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.ie-duo__option {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  padding: 0 14px 0 10px;
  border: 1px solid #d3d8e0;
  border-radius: 999px;
  background: #fff;
  color: #1e2430;
  font-size: .9rem;
  cursor: pointer;
}
.ie-duo__option input {
  position: absolute;
  inset: 0;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}
.ie-duo__option:has(input:checked) {
  border-color: #1e2430;
  box-shadow: inset 0 0 0 1px #1e2430;
  font-weight: 600;
}
.ie-duo__option:has(input:focus-visible) {
  outline: 3px solid #3b5bdb;
  outline-offset: 2px;
}
.ie-duo__sw {
  display: flex;
  width: 28px;
  height: 20px;
  border-radius: 6px;
  overflow: clip;
}
.ie-duo__sw i {
  flex: 1;
}
.ie-duo__option--harbor i:first-child {
  background: #13235b;
}
.ie-duo__option--harbor i:last-child {
  background: #ff8f70;
}
.ie-duo__option--mint i:first-child {
  background: #0f3b33;
}
.ie-duo__option--mint i:last-child {
  background: #a6f0c6;
}
.ie-duo__option--ink i:first-child {
  background: #2d0f45;
}
.ie-duo__option--ink i:last-child {
  background: #f9d348;
}

Multiply tint with safe text contrast

  • background-blend-mode
  • multiply
  • one element

Coastline Film Week

12 to 15 June · Old Harbor Cinema

Get festival passes
Tint color

One element carries both the photo and a background color, and background-blend-mode: multiply tints the photo with no overlay markup. Multiply can only darken, so no pixel ends up lighter than the tint itself. If white text passes contrast against the tint color, it passes everywhere on the banner.

View code
.ie-tint {
  --ie-tint: #7c2d12;
  display: grid;
  gap: 12px;
  width: min(100%, 480px);
}
.ie-tint:has(input[value="navy"]:checked) {
  --ie-tint: #1e3a8a;
}
.ie-tint:has(input[value="forest"]:checked) {
  --ie-tint: #14532d;
}
.ie-tint__hero {
  display: grid;
  align-content: end;
  justify-items: start;
  gap: 6px;
  min-height: 270px;
  padding: 26px 24px;
  border-radius: 18px;
  background: var(--ie-tint) url("coast-road.webp") center / cover no-repeat;
  background-blend-mode: multiply;
  color: #fff;
  transition: background-color .4s ease;
}
.ie-tint__title {
  margin: 0;
  font-family: "Fraunces", serif;
  font-size: clamp(1.7rem, 6vw, 2.4rem);
  font-weight: 600;
  line-height: 1.05;
}
.ie-tint__date {
  margin: 0 0 10px;
  font-size: .95rem;
}
.ie-tint__cta {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 18px;
  border-radius: 999px;
  background: #fff;
  color: var(--ie-tint);
  font-weight: 700;
  text-decoration: none;
}
.ie-tint__cta:hover {
  background: #f3efe9;
}
.ie-tint__cta:focus-visible {
  outline: 3px solid #fff;
  outline-offset: 3px;
}
.ie-tint__picker {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
}
.ie-tint__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.ie-tint__option {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  padding: 0 14px 0 10px;
  border: 1px solid #d6d1ca;
  border-radius: 12px;
  background: #fff;
  color: #2a2420;
  font-size: .9rem;
  cursor: pointer;
}
.ie-tint__option input {
  position: absolute;
  inset: 0;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}
.ie-tint__option span {
  width: 20px;
  height: 20px;
  border-radius: 50%;
}
.ie-tint__option--rust span {
  background: #7c2d12;
}
.ie-tint__option--navy span {
  background: #1e3a8a;
}
.ie-tint__option--forest span {
  background: #14532d;
}
.ie-tint__option:has(input:checked) {
  border-color: #2a2420;
  box-shadow: inset 0 0 0 1px #2a2420;
  font-weight: 600;
}
.ie-tint__option:has(input:focus-visible) {
  outline: 3px solid #2563eb;
  outline-offset: 2px;
}

Halftone dot screen poster

  • hard-stop radial-gradient
  • mix-blend-mode
  • background-size
A narrow brick alley with fire escapes and ivy, office towers rising at the far end

Back Alley Sessions

Live sets every Friday from 9 pm

A grayscale photo multiplied into a red frame gives the print color, and a 6px tile of radial-gradient with a hard stop lays a dot screen on top. The dots multiply too, so they darken the image without hiding it. The checkbox turns the screen off for comparison.

View code
.ie-dots {
  display: grid;
  justify-items: center;
  gap: 14px;
  width: min(100%, 320px);
}
.ie-dots__poster {
  width: 100%;
  padding: 14px 14px 18px;
  background: #f1ebdf;
  color: #17130f;
}
.ie-dots__frame {
  position: relative;
  aspect-ratio: 4 / 5;
  overflow: clip;
  background: #e2432b;
  isolation: isolate;
}
.ie-dots__frame img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: grayscale(1) contrast(1.35) brightness(1.1);
  mix-blend-mode: multiply;
}
.ie-dots__frame::after {
  content: "";
  position: absolute;
  inset: 0;
  background-image: radial-gradient(circle, #1a1410 0 1.3px, transparent 1.7px);
  background-size: 6px 6px;
  mix-blend-mode: multiply;
  pointer-events: none;
  transition: opacity .3s ease;
}
.ie-dots:has(input:not(:checked)) .ie-dots__frame::after {
  opacity: 0;
}
.ie-dots__title {
  margin: 14px 0 2px;
  font-family: "Fraunces", serif;
  font-size: 1.6rem;
  font-weight: 700;
  line-height: 1.05;
  text-transform: uppercase;
}
.ie-dots__sub {
  margin: 0;
  color: #4a3f35;
  font-size: .9rem;
}
.ie-dots__switch {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  min-height: 44px;
  padding: 0 16px 0 12px;
  border: 1px solid #3a3f4a;
  border-radius: 999px;
  color: #e8eaee;
  font-size: .9rem;
  cursor: pointer;
}
.ie-dots__switch input {
  width: 20px;
  height: 20px;
  margin: 0;
  accent-color: #e2432b;
  cursor: pointer;
}
.ie-dots__switch:has(input:focus-visible) {
  outline: 3px solid #f1ebdf;
  outline-offset: 2px;
}

Photo knocked into the headline

  • background-clip: text
  • @supports
  • cqi units

Canyon

Seven days on the red rock trail · 14 to 21 September

The photo is the background of the headline itself, and background-clip: text trims it to the letter shapes. The word stays real text, so it can be selected, translated and read by screen readers. Without support, the @supports block is skipped and a solid canyon orange shows instead.

View code
.ie-knock {
  container-type: inline-size;
  width: min(100%, 560px);
  padding: 28px 0 24px;
  border-radius: 18px;
  background: #f6efe6;
  text-align: center;
}
.ie-knock__word {
  margin: 0;
  color: #e2733b;
  font-family: "Fraunces", serif;
  font-size: min(20cqi, 8.5rem);
  font-weight: 800;
  line-height: .95;
  letter-spacing: -.02em;
  text-transform: uppercase;
}
@supports (background-clip: text) or (-webkit-background-clip: text) {
  .ie-knock__word {
    background: url("red-rock-canyon.webp") 50% 28% / cover no-repeat;
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
  }
}
.ie-knock__sub {
  margin: 14px 0 0;
  padding: 0 16px;
  color: #4a3a2e;
  font-size: .95rem;
}

Masks, clip-path and cropping

Three ways to choose which part of an image shows: mask-image for soft or file-based shapes, clip-path for hard edges that animate, and object-position for keeping the subject inside a crop.

Blob, soft circle and fade masks

  • mask-image
  • SVG data URI
  • -webkit-mask
  • Red rock mesa glowing orange at sunset above a boulder-strewn slope url(blob.svg)
  • A waterfall dropping into a green gorge, cropped to a soft-edged circle radial-gradient()
  • A blue fjord between granite cliffs, fading out toward the bottom linear-gradient()

Three masks on three square photos. The first uses an SVG blob inlined as a data URI, so the mask needs no extra request and no CORS headers, the second a radial-gradient with a soft edge, the third a linear-gradient fade. Opaque parts of the mask show the image and transparent parts hide it, and the -webkit- line covers older Safari and Chromium builds.

View code
.ie-shapes {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 14px;
  width: min(100%, 560px);
  margin: 0;
  padding: 0;
  list-style: none;
}
.ie-shapes__item {
  display: grid;
  justify-items: center;
  gap: 10px;
  min-width: 0;
}
.ie-shapes__item img {
  display: block;
  width: 100%;
  aspect-ratio: 1;
  height: auto;
  object-fit: cover;
}
.ie-shapes__blob {
  --ie-blob: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 200'%3E%3Cpath transform='translate(100 100)' d='M45.6,-58.7C58.3,-46.8,67.3,-31.4,71.2,-14.3C75.1,2.8,73.9,21.6,65.1,36.1C56.3,50.6,39.9,60.8,22.1,67.4C4.3,74,-14.9,77,-31.4,70.8C-47.9,64.6,-61.7,49.2,-69.6,31.4C-77.5,13.6,-79.5,-6.6,-73.2,-23.8C-66.9,-41,-52.3,-55.2,-36.3,-66.3C-20.3,-77.4,-2.8,-85.4,13.6,-82.9C30,-80.4,32.9,-70.6,45.6,-58.7Z'/%3E%3C/svg%3E");
  -webkit-mask: var(--ie-blob) center / contain no-repeat;
  mask: var(--ie-blob) center / contain no-repeat;
}
.ie-shapes__soft {
  -webkit-mask-image: radial-gradient(closest-side, #000 72%, transparent 100%);
  mask-image: radial-gradient(closest-side, #000 72%, transparent 100%);
}
.ie-shapes__fade {
  border-radius: 14px 14px 0 0;
  -webkit-mask-image: linear-gradient(#000 45%, transparent);
  mask-image: linear-gradient(#000 45%, transparent);
}
.ie-shapes__item code {
  max-width: 100%;
  color: #2c3440;
  font: .72rem/1.3 ui-monospace, Consolas, monospace;
  overflow-wrap: anywhere;
  text-align: center;
}

Hexagon to frame clip-path reveal

  • clip-path: polygon()
  • matching point count
  • :focus-visible

At rest the photo is clipped to a six-point hexagon. On hover or focus it opens to a rectangle that also has six points, two of them sitting on the side edges, so every point has a partner and the shape interpolates instead of snapping.

View code
.ie-clip {
  display: grid;
  gap: 4px;
  width: min(100%, 380px);
  color: #f2efe9;
  text-decoration: none;
}
.ie-clip__frame {
  display: block;
  aspect-ratio: 4 / 3;
  margin-bottom: 10px;
}
.ie-clip__frame img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  clip-path: polygon(25% 0, 75% 0, 100% 50%, 75% 100%, 25% 100%, 0 50%);
}
.ie-clip:hover .ie-clip__frame img,
.ie-clip:focus-visible .ie-clip__frame img {
  clip-path: polygon(0 0, 100% 0, 100% 50%, 100% 100%, 0 100%, 0 50%);
}
.ie-clip__title {
  font-family: "Fraunces", serif;
  font-size: 1.4rem;
  font-weight: 600;
  line-height: 1.2;
}
.ie-clip__meta {
  color: #b9b4ab;
  font-size: .9rem;
}
.ie-clip:focus-visible {
  outline: 3px solid #f2c14e;
  outline-offset: 6px;
  border-radius: 4px;
}
@media (prefers-reduced-motion: no-preference) {
  .ie-clip__frame img {
    transition: clip-path .6s cubic-bezier(.2, .7, .2, 1);
  }
}

object-position focal point crops

  • object-fit: cover
  • object-position
  • aspect-ratio
Two yellow tents half buried in snow beside a dry stone wall, jagged snowy peaks behind
3:1
1:1
9:16

One photo cropped into three aspect ratios with object-fit: cover. The default center crop cuts the tents out of the wide banner and half of them out of the tall one. Tick the box to set object-position to the focal point (76% 90%), which keeps the subject in every crop.

View code
.ie-focus {
  --ie-pos: 50% 50%;
  display: grid;
  justify-items: start;
  gap: 12px;
  width: min(100%, 460px);
}
.ie-focus:has(input:checked) {
  --ie-pos: 76% 90%;
}
.ie-focus__crops {
  display: grid;
  grid-template-columns: minmax(0, 16fr) minmax(0, 9fr);
  gap: 10px;
  width: 100%;
}
.ie-focus__crop {
  position: relative;
  margin: 0;
  border-radius: 12px;
  overflow: clip;
}
.ie-focus__crop img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--ie-pos);
}
.ie-focus__crop--wide {
  grid-column: 1 / -1;
  aspect-ratio: 3 / 1;
}
.ie-focus__crop--square {
  aspect-ratio: 1;
}
.ie-focus__crop--tall {
  aspect-ratio: 9 / 16;
}
.ie-focus__crop figcaption {
  position: absolute;
  top: 8px;
  left: 8px;
  padding: 2px 8px;
  border-radius: 6px;
  background: rgba(12, 18, 28, .82);
  color: #fff;
  font: 600 .75rem/1.5 ui-monospace, Consolas, monospace;
}
.ie-focus__switch {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  min-height: 44px;
  color: #1d2531;
  font-size: .95rem;
  cursor: pointer;
}
.ie-focus__switch input {
  width: 20px;
  height: 20px;
  margin: 0;
  accent-color: #1d4ed8;
  cursor: pointer;
}
.ie-focus__switch input:focus-visible {
  outline: 3px solid #1d4ed8;
  outline-offset: 3px;
}
@media (prefers-reduced-motion: no-preference) {
  .ie-focus__crop img {
    transition: object-position .6s ease;
  }
}

Hover and interactive image effects

Effects that react to the visitor. Every hover state also runs on keyboard focus, and anything that moves sits inside a prefers-reduced-motion check.

Zoom inside the frame

  • overflow: clip
  • transform: scale()
  • :focus-visible

Hover, focus with Tab, or drag.

The frame clips overflow, the img carries the transition, and hover or focus scales it to 1.12 with a slight tilt. transform only changes how the image is painted, so neighbors never shift. With reduced motion requested the tiles stay still and only the caption color changes.

View code
.ie-zoom {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 14px;
  width: min(100%, 480px);
  margin: 0;
  padding: 0;
  list-style: none;
}
.ie-zoom__tile {
  display: grid;
  gap: 2px;
  color: #172030;
  text-decoration: none;
}
.ie-zoom__frame {
  display: block;
  aspect-ratio: 4 / 5;
  margin-bottom: 8px;
  border-radius: 14px;
  overflow: clip;
}
.ie-zoom__frame img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.ie-zoom__name {
  font-weight: 700;
  transition: color .3s ease;
}
.ie-zoom__meta {
  color: #566172;
  font-size: .85rem;
}
.ie-zoom__tile:hover .ie-zoom__name,
.ie-zoom__tile:focus-visible .ie-zoom__name {
  color: #b4441c;
}
.ie-zoom__tile:focus-visible {
  outline: none;
}
.ie-zoom__tile:focus-visible .ie-zoom__frame {
  outline: 3px solid #b4441c;
  outline-offset: 3px;
}
@media (prefers-reduced-motion: no-preference) {
  .ie-zoom__frame img {
    transition: transform .6s cubic-bezier(.2, .7, .2, 1);
  }
  .ie-zoom__tile:hover .ie-zoom__frame img,
  .ie-zoom__tile:focus-visible .ie-zoom__frame img {
    transform: scale(1.12) rotate(1.5deg);
  }
}

Caption panel sliding up

  • translate
  • hover: none
  • solid translucent panel

Hover, focus with Tab, or drag.

Only the title peeks out at rest. Hover or focus slides the whole caption panel up, and the panel uses a near-opaque fill so the text keeps its contrast over any part of the photo. On touch screens the panel starts open, because hover never fires there.

View code
.ie-cap {
  position: relative;
  display: block;
  width: min(100%, 420px);
  aspect-ratio: 4 / 3;
  border-radius: 18px;
  overflow: clip;
  color: #fff;
  text-decoration: none;
}
.ie-cap > img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.ie-cap__panel {
  position: absolute;
  right: 0;
  bottom: 0;
  left: 0;
  display: grid;
  gap: 8px;
  padding: 16px 18px 18px;
  background: rgba(14, 17, 22, .88);
  translate: 0 calc(100% - 2.9rem);
}
.ie-cap__title {
  font-family: "Fraunces", serif;
  font-size: 1.2rem;
  font-weight: 600;
  line-height: 1.2;
}
.ie-cap__text {
  color: #dfe3e8;
  font-size: .92rem;
  line-height: 1.45;
}
.ie-cap__more {
  color: #ffd27a;
  font-size: .9rem;
  font-weight: 600;
}
.ie-cap:hover .ie-cap__panel,
.ie-cap:focus-visible .ie-cap__panel {
  translate: 0 0;
}
.ie-cap:focus-visible {
  outline: 3px solid #1f2a37;
  outline-offset: 4px;
}
@media (hover: none) {
  .ie-cap__panel {
    translate: 0 0;
  }
}
@media (prefers-reduced-motion: no-preference) {
  .ie-cap__panel {
    transition: translate .45s cubic-bezier(.2, .7, .2, 1);
  }
}

Before and after comparison slider

  • input type="range"
  • clip-path: inset()
  • custom property
A granite cliff above pine trees and a still river, shown before and after color grading

Hover, focus with Tab, or drag.

Two copies of the photo stacked, the top one with a flat unedited filter and a clip-path: inset() driven by one custom property. A transparent range input covers the frame, so dragging, clicking and the arrow keys all work, and the browser announces the value for free.

View code
.ie-compare {
  --ie-pos: 50%;
  position: relative;
  width: min(100%, 520px);
  aspect-ratio: 4 / 3;
  border-radius: 16px;
  overflow: clip;
}
.ie-compare > img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.ie-compare__after {
  filter: saturate(1.15) contrast(1.06);
}
.ie-compare__before {
  filter: saturate(.35) contrast(.8) brightness(1.08);
  clip-path: inset(0 calc(100% - var(--ie-pos)) 0 0);
}
.ie-compare__tag {
  position: absolute;
  top: 12px;
  padding: 3px 10px;
  border-radius: 999px;
  background: rgba(12, 16, 22, .8);
  color: #fff;
  font-size: .78rem;
  font-weight: 600;
  pointer-events: none;
}
.ie-compare__tag--before {
  left: 12px;
}
.ie-compare__tag--after {
  right: 12px;
}
.ie-compare__handle {
  position: absolute;
  top: 0;
  bottom: 0;
  left: var(--ie-pos);
  width: 3px;
  background: #fff;
  translate: -50% 0;
  pointer-events: none;
}
.ie-compare__handle::after {
  content: "\2039\00a0\203A";
  position: absolute;
  top: 50%;
  left: 50%;
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: #fff;
  color: #18202b;
  font-size: 1.3rem;
  font-weight: 700;
  line-height: 1;
  translate: -50% -50%;
  box-shadow: 0 4px 14px rgba(0, 0, 0, .35);
}
.ie-compare__range {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  cursor: ew-resize;
}
.ie-compare__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.ie-compare:has(.ie-compare__range:focus-visible) .ie-compare__handle::after {
  outline: 3px solid #ffd43b;
  outline-offset: 3px;
}

Pointer spotlight, opt-in

  • mask-image
  • CSS variables from JS
  • aria-pressed
An orange jellyfish trailing long red tentacles through deep blue water

Hover, focus with Tab, or drag.

A dark shade covers the photo, and a radial-gradient mask punches a hole in it at --ie-x and --ie-y. A few lines of script copy the pointer position into those variables. The effect is off until the visitor presses the button, and the arrow keys move the light for keyboard users.

View code
.ie-spot {
  container-type: inline-size;
  display: grid;
  justify-items: center;
  gap: 14px;
  width: min(100%, 480px);
}
.ie-spot__frame {
  --ie-x: 56%;
  --ie-y: 34%;
  --ie-r: min(22cqi, 100px);
  position: relative;
  width: 100%;
  aspect-ratio: 4 / 3;
  border-radius: 16px;
  overflow: clip;
}
.ie-spot__frame img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.ie-spot__shade {
  position: absolute;
  inset: 0;
  background: rgba(3, 8, 24, .92);
  opacity: 0;
  pointer-events: none;
  transition: opacity .35s ease;
  -webkit-mask-image: radial-gradient(circle at var(--ie-x) var(--ie-y), transparent 0 var(--ie-r), #000 calc(var(--ie-r) + 36px));
  mask-image: radial-gradient(circle at var(--ie-x) var(--ie-y), transparent 0 var(--ie-r), #000 calc(var(--ie-r) + 36px));
}
.ie-spot:has(.ie-spot__toggle[aria-pressed="true"]) .ie-spot__shade {
  opacity: 1;
}
.ie-spot__frame:focus-visible {
  outline: 3px solid #7cc4ff;
  outline-offset: 3px;
}
.ie-spot__toggle {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  min-height: 44px;
  padding: 0 18px 0 8px;
  border: 1px solid #3c4658;
  border-radius: 999px;
  background: #151b26;
  color: #eef2f8;
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}
.ie-spot__knob {
  position: relative;
  width: 40px;
  height: 24px;
  border-radius: 999px;
  background: #3c4658;
  transition: background-color .25s ease;
}
.ie-spot__knob::after {
  content: "";
  position: absolute;
  top: 3px;
  left: 3px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: #fff;
}
.ie-spot__toggle[aria-pressed="true"] .ie-spot__knob {
  background: #2f80ed;
}
.ie-spot__toggle[aria-pressed="true"] .ie-spot__knob::after {
  left: 19px;
}
.ie-spot__toggle:focus-visible {
  outline: 3px solid #7cc4ff;
  outline-offset: 3px;
}
@media (prefers-reduced-motion: no-preference) {
  .ie-spot__knob::after {
    transition: left .25s ease;
  }
}

Which CSS property creates which image effect?

Filter handles color and blur, plus shadows when you need them. Blend modes tint and layer, clip-path and mask-image reshape, and object-fit frames the picture inside its box. Motion comes from transition with transform, and backdrop-filter frosts whatever sits behind an element.

GoalPropertyExample valueBaseline status
Recolor, blur, add a shadowfiltergrayscale(100%)Widely available
Tint or layermix-blend-mode, background-blend-modemultiplyWidely available
Crop or reshapeclip-path, object-fitcircle(40%), coverWidely available
Fade edges, cut out shapesmask-imagelinear-gradient(black, transparent)Widely available (since June 2026)
Frost what sits behindbackdrop-filterblur(8px)Newly available in 2024

Status labels come from the web-features dataset. The newest row, backdrop-filter, deserves a fresh look at its Baseline label before it goes on a production page.

Motion sits on top of every row. The transition property has worked across browsers since September 2015 (MDN Web Docs), and it animates any property with an interpolable value, filter and transform included.

Reach for filter when the pixels themselves need a new color or sharpness. A blend mode fits when the image has to relate to another layer underneath it. If the real question is which part of the image shows, you’re looking at clip-path, mask-image or object-fit, and anything that moves belongs to transition and transform.

None of these touches the file on disk. The browser applies the effect at render time, so deleting the rule brings the original back.

One opinion, though. If an effect is permanent and the image set is fixed (a grayscale client-logo strip, say), bake it into the file, since a pre-edited image adds no render-time work.

How do CSS filter functions change an image?

The filter property takes a list of functions and applies them to an element. The available ones are blur(), brightness(), contrast(), drop-shadow(), grayscale(), hue-rotate(), invert(), opacity(), saturate() and sepia().

The property has held Baseline Widely Available status since March 2019 (web-features dataset, Web Platform DX), so all 10 functions run across the core browsers.

Most take a single value. blur() wants a length (blur(5px)), hue-rotate() wants an angle (hue-rotate(90deg)), and the tone functions accept percentages or plain numbers (contrast(200%), brightness(0.4)), per MDN Web Docs. drop-shadow() is the odd one out, with offsets, a blur radius and a color.

Color and tone functions

grayscale() runs from 0%, which leaves color alone, to 100%, which removes all of it. sepia() adds a warm brown cast (MDN’s own example uses sepia(60%)), and hue-rotate() shifts every hue around the color wheel by whatever angle you give it.

For brightness(), contrast() and saturate(), 100% is neutral, so values above or below push the picture in either direction. invert() flips every channel, so invert(100%) gives a negative.

Bennett Feely’s image effects library leans on grayscale(), brightness() and contrast() to refine its blend-mode effects, and its notes say the values need tweaking per photo. That matches how this works in practice. A setting that flatters a landscape can crush a portrait.

Blur and soft focus

blur() takes a length, never a percentage.

Low values soften an image, and high values turn it into a color wash behind text. A fuller walkthrough of blur-on-hover and blurred backgrounds lives in this guide to CSS blur effects.

One catch. filter applies to the whole element, border included. MDN’s own example greys and blurs the border along with the image, so put borders on a wrapper if they should stay sharp.

Drop-shadow versus box-shadow on images

box-shadow traces the rectangular box and accepts spread and inset. drop-shadow() follows the alpha channel of the image itself, and it rejects inset and spread (MDN Web Docs).

A transparent PNG or SVG cutout gets a shadow in its real silhouette from drop-shadow(). A plain rectangular photo gets a rectangle from either one, so box-shadow is the simpler pick there, and a box-shadow generator speeds up tuning the offsets.

Chaining several functions

Functions run in the order written, and each one receives the output of the one before it, according to MDN.

That makes contrast(175%) brightness(103%) a different recipe from brightness(103%) contrast(175%). When a chain needs tuning by eye, a point-and-click filter generator shows the result as the sliders move.

How to build image hover effects with transition and transform

Start with a container that clips overflow. The transition goes on the image’s normal state, and the changed values go under :hover. Declaring the transition on the normal state is what makes the effect animate back out when the pointer leaves.

  1. Wrap the img in a container and set overflow: hidden, so a zoomed image stays inside its frame.
  2. Add transition: transform 0.4s ease to the img itself, not to the :hover rule.
  3. Under the container’s :hover state, set the img to transform: scale(1.1).
  4. For a color change, list filter in the same transition and give the two states matching values, such as grayscale(100%) at rest and grayscale(0%) on hover.
  5. Check the resting state on a touch screen, where hover never fires, and make sure the image still reads well without it.

A few details from MDN trip people up. In the transition shorthand, the first time value is the duration and the second is the delay. And transition-property defaults to all, so a bare duration animates everything animatable. Name transform and filter instead.

Zoom and rotate on hover

transform changes how the image is painted, not where it sits in the layout, so a scaled image never pushes its neighbors around.

scale(1.1) is a safe zoom. Past 1.2 the crop starts hiding the subject. Add rotate(2deg) for a tilt, and keep the duration between 0.3s and 0.6s so the motion reads as deliberate. I’d skip the zoom entirely on any image with text baked into it, since the edges get cut off fast.

Beaver Builder’s documentation has offered ready-made zoom, zoom-and-rotate and unblur hover classes for its Photo module, with transitions set to 0.3s.

Cards, buttons and links follow the same pattern, and this roundup of CSS hover effects collects variations beyond images.

Color change on hover

MDN Web Docs lists the animation type of filter as a filter function list, so each function interpolates on its own value.

Grayscale-to-color is grayscale(100%) at rest and grayscale(0%) on hover. Blur-to-sharp works the same way, from blur(4px) to blur(0).

Keep the same functions in the same order in both states and change only the values. Every pair then has a partner to animate toward.

Ken Burns effect with keyframes

The Ken Burns effect is a slow zoom and drift that runs without any pointer input.

It needs @keyframes rather than a transition, because the motion starts on load and loops. If the syntax is new, this primer on CSS keyframes covers it.

The effect itself is one rule, animation: drift 20s ease-in-out infinite alternate, paired with @keyframes drift that moves transform from scale(1) to scale(1.15) and a small translate. Put the image inside an overflow: hidden frame so the zoom crops instead of spilling.

Slow motion is still motion for visitors who set prefers-reduced-motion, so the animation needs an off switch.

How do overlays, blend modes and duotone effects work?

Stack a colored or gradient layer above an image and you have an overlay. A blend mode then decides how that layer’s pixels combine with the image underneath.

Duotone is the best-known result, built by graying the image and blending two color layers over it.

Color and gradient overlays

Build the overlay on a wrapper’s ::after pseudo-element, so the markup stays a single img inside a container. Give it position: absolute with inset: 0 so the layer matches the image box, and add pointer-events: none so clicks pass through to any link beneath. Keep its z-index below the caption text.

A bottom-weighted gradient, transparent at the top and dark at the bottom, keeps caption text readable without dimming the whole photo. A visual gradient generator speeds up tuning the stops.

For a caption reveal, start the caption at opacity: 0 and raise it to 1 on :hover and :focus-within. The :focus-within half gives keyboard users the same reveal.

mix-blend-mode versus background-blend-mode

mix-blend-mode blends an element with the content rendered behind it in the same stacking context (MDN Web Docs), which makes it the one to use on an img placed over a colored container.

background-blend-mode is narrower. It mixes an element’s own background layers with each other and with its background-color, so it can tint a CSS background image with no extra markup.

The values for background-blend-mode follow the order of background-image, and a shorter list repeats or truncates until the lengths match, per MDN. The keywords people reach for are multiply (darkens), screen (lightens), overlay (boosts contrast) and difference (inverts where layers disagree).

Blend mode changes are not interpolated (MDN), so a hover that swaps multiply for screen snaps instead of fading. Fade the overlay’s opacity and keep the blend mode fixed.

You’ll see mix-blend-mode quoted with two different support dates. MDN says it has worked across browsers since January 2020, while web-features marks it Baseline Widely Available since 2022-07-15. Both are right: Edge 79, released 2020-01-15, was the last engine to ship it, and the later date falls exactly 30 months afterward.

Duotone with blend modes

Duotone maps a photo’s shadows to one color and its highlights to another.

The CSS version grays the image, then layers two solid colors over it with the multiply and lighten blend modes (darken can replace multiply when the result runs too dark), in pure CSS with no script. Bright colors on images with large areas of white or black give the best results, according to Web Designer Wall.

Spotify built its brand identity around duotone photography, and its Colorizer tool kept thousands of uploaded images consistent with the brand (Web Designer Wall, 2017). Text laid over a duotone image needs its own contrast check, because the effect shifts brightness relationships across the whole photo.

How to crop and shape images with clip-path, mask-image and object-fit

object-fit controls how an image fills its box. clip-path cuts hard-edged shapes, and mask-image handles soft or gradient-based edges.

All three change which part of the picture shows without touching the file, and they combine freely. An object-fit: cover image inside a clip-path circle is the standard avatar.

Fitting images to a box with object-fit

object-fit takes a single keyword: fill, contain, cover, none or scale-down (MDN Web Docs). fill is the initial value.

fill stretches the image to the box, which distorts it when the ratios differ. contain scales the whole image to fit inside and leaves empty space on one axis. cover fills the box and clips whatever overflows, and it’s the usual pick for thumbnails and cards.

none keeps the natural size, and scale-down picks whichever of none or contain is smaller. The property targets replaced elements such as img and video, and MDN notes it does nothing on iframe, embed or fencedframe.

With cover, object-position decides which part of the image survives the crop. Setting object-position: top, or a percentage pair, keeps a face or a product in frame when the default center crop would cut it off. Unlike CSS cropping, which only changes how an image appears in the browser, a cropping pictures app lets you permanently adjust the image’s composition before uploading it to your website.

Clip-path shapes

clip-path hides everything outside the shape you give it.

The shape functions are circle(), ellipse(), inset() and polygon(), plus path(), rect(), xywh() and shape() (MDN Web Docs). circle(40%) gives a round crop, and polygon(50% 0, 100% 100%, 0 100%) gives a triangle.

Placing polygon points by hand is slow, so a visual clip-path generator pays off for anything beyond a circle.

clip-path animates with transitions and animations, which makes reveals cheap to build. Start at inset(0 100% 0 0), end at inset(0), and keep the same shape function (and the same number of polygon points) in both states.

Watch out for the shadow, though. clip-path also clips the element’s own shadow, so put box-shadow or drop-shadow() on a wrapper if the cropped shape needs one.

Gradient masks

mask-image hides parts of an element based on the alpha channel of the mask image and, depending on mask-mode, its luminance (MDN Web Docs). Opaque areas of the mask show the image, and transparent areas hide it.

A gradient is the simplest mask. mask-image: linear-gradient(black, transparent) fades an image out toward the bottom, and radial-gradient(black 50%, transparent 60%) gives a round image with a soft edge, both patterns taken from MDN’s examples.

mask-image works like background-image, so it accepts a gradient, an image file or an SVG, and a file-based mask needs a transparent or semi-transparent area to do anything (web.dev). An SVG file or an inline SVG data URI helps when the mask is an irregular shape. File-based masks only load over HTTP or HTTPS, so a mask tested from a file:// URL renders as transparent black (MDN Web Docs).

MDN lists the animation type of mask-image as discrete, so a transition between two masks snaps instead of fading. Fade the wrapper’s opacity on hover and leave the mask static.

When do backdrop-filter and SVG filters beat the filter property?

Reach for backdrop-filter when the effect belongs to what sits behind an element, and for an SVG filter when no CSS function can produce the result, such as an exact two-color duotone or warped pixels.

NeedToolWhy it fits
Frosted caption bar over a photobackdrop-filterAffects the area behind the element
Exact two-color duotonefeColorMatrixMaps colors with matrix values
Warped or glitched pixelsfeDisplacementMapShifts pixels using a second input
Plain blur, grayscale, shadowfilterOne declaration, no extra file

The last row is the honest default. Most pages never need the other three.

backdrop-filter for frosted glass

The web-features dataset defines backdrop-filter as applying blur or color shifts to the area behind an element, where filter changes the element itself.

It takes the same function list as filter, so backdrop-filter: blur(8px) on a semi-transparent panel produces the frosted bar seen on image cards. The panel needs a translucent background, or the blur has nothing to show through.

Frosted panels are the core of glassmorphism, and this walkthrough of CSS glassmorphism builds full card layouts on the same property.

SVG filter primitives

An SVG filter is a chain of primitives (elements prefixed with fe) defined once and applied with filter: url(file.svg#id). The primitives hand results to each other through result and in attributes, as MDN’s feBlend example does with feFlood.

feColorMatrix remaps color channels, which is how an exact two-color duotone gets built. feDisplacementMap moves pixels in one input by the values of a second input, and that’s the basis of distortion and glitch looks. feTurbulence generates noise, usually to feed the displacement map (MDN’s own example pairs the two), and feBlend applies blend modes inside the filter.

José M. Pérez’s 2016 write-up applies a duotone to an image with feColorMatrix, computing the matrix in JavaScript from the target colors, and points to CSS blend modes as the alternative that needs no JavaScript.

The price of an SVG filter is upkeep, since it lives in markup that few teams read fluently. Defining it in an inline block, as this guide to SVG in HTML describes, keeps it next to the image that uses it.

Which browsers support CSS image effects, and what fallbacks work?

Every property covered here runs in current Chrome, Edge, Firefox and Safari. Fallbacks matter for older engines and for the newest entry, backdrop-filter.

Current support figures

As of October 2026, these are the figures worth knowing:

  • clip-path has been Baseline Widely Available since 2023-07-21 (web-features).
  • mask has been Baseline Widely Available since 2026-06-07 (web-features), and it appears among features that reached Widely Available in June 2026 (web.dev Baseline digest).
  • Safari 18 completed the set of engines shipping backdrop-filter, with Widely Available projected for 2027-03-16 (web-features).
  • backdrop-filter appears in about 37% of page loads (Chrome Platform Status, October 2026).

The basic shapes behind most image crops (circle(), ellipse(), inset(), polygon()) cleared the Widely Available bar earlier than the full clip-path property. Cropping with them sits at the safe end.

Sources now agree on mask. The web-features explorer page carried the Newly Available label from 2023 with a projected Widely Available date of 2026-06-07, and web.dev’s June 2026 digest, published on July 17, 2026, lists the mask property among features that reached Widely Available. The comparison table at the top of this article uses the updated label.

The projected date has passed, so older guides that still call mask newly available are out of date. MDN’s wording for the Newly Available label is that a feature works in the latest browser versions and can fail in older ones, and that is the label backdrop-filter still carries.

Fallback patterns

Write the unaffected image as the base style and put each effect inside @supports. Bennett Feely’s image effects library works this way, showing the unedited source image by default and turning an effect on only when the browser passes the check.

Masks need a second declaration. web.dev notes that most mask-image features reach older browsers through -webkit-mask-image, so write both properties. For frosted panels, use @supports not (backdrop-filter: blur(1px)) to swap in a more opaque background, so text stays readable where the blur is missing.

Tables only go so far, and cross-browser compatibility checks in real engines catch what they miss. Start with Safari, the last engine to ship backdrop-filter.

When do image effects hurt performance or accessibility?

Effects get expensive when they repaint large areas or run on many elements at once. They turn into an accessibility problem when they move and can’t be stopped.

Rendering cost

web.dev advises restricting animation to opacity and transform wherever possible, because those properties stay on the compositing stage of the rendering path.

For any other property, check which stage it hits before shipping. Chrome’s Lighthouse flags non-composited animations, meaning any animation that triggers Style, Layout or Paint, and notes that they can also raise Cumulative Layout Shift.

F22 Labs’ case study of a blur that slowed a video interface found the cost followed how much area the blur covered and how large the radius was. SitePoint warns that backdrop-filter on many elements, or across a large area, drags performance down. And MDN calls will-change a last resort for existing performance problems, since heavy use costs memory and forces more complex rendering.

Chrome answers certain will-change values by creating a compositing layer or a stacking context. MDN suggests setting the property from script just before a change instead of leaving it in the stylesheet.

How to find the expensive effect

  1. Open the Rendering panel in Chrome DevTools (More tools, then Rendering).
  2. Switch on Paint flashing, which flashes repainted regions in green, and trigger the effect.
  3. Turn on Layer borders to see compositor layers outlined in orange and olive, with tiles in cyan, and look for layers nobody asked for.
  4. Record in the Performance panel and open the paint profiler if a frame runs long.
  5. Run Lighthouse and read its list of non-composited animations.

Chrome’s DevTools documentation describes the green paint-flashing overlay and the layer-border colors, and web.dev points to the paint profiler for spotting costly paint work. A layer that flashes green with no user input is repainting for nothing.

Contrast and motion

WCAG 1.4.3 sets a minimum contrast ratio of 4.5:1 for text, or 3:1 for large text (W3C). The criterion covers text revealed on hover or keyboard focus, so hover captions count.

Overlays and blend modes change the backdrop under that text, so measure color contrast at the lightest patch behind the words, not at the average of the image.

On reduced motion, MDN describes the prefers-reduced-motion media feature as detecting a device setting that minimizes non-essential motion. Its example uses @media (prefers-reduced-motion: reduce) to swap a scaling animation for a gentler dissolve.

MDN also describes the setting as a request to remove, reduce or replace motion, so reduce does not mean stripping every animation. WCAG 2.3.3 (Level AAA) asks that motion triggered by interaction can be disabled unless it is essential to the function or the information conveyed. For image effects, scale, rotate and drift are the motion to remove, while a color fade can stay.

Auto-playing loops run into WCAG 2.2.2, which requires a way to pause, stop or hide moving content that starts automatically, lasts longer than five seconds and sits beside other content. A looping Ken Burns drift qualifies, and W3C adds that hover- or focus-triggered motion with no way to stop it can fail 2.2.2 and possibly 2.3.3.

Run the finished page through a web accessibility checklist before release, since contrast and motion are only two of its lines.

When CSS is the wrong tool

Skip CSS for a full-width, always-on blur and export a pre-blurred copy at build time instead. Charts, maps and screenshots where color carries meaning should stay unfiltered. In a long gallery, run effects on hover or focus only, never as always-on filters.

If an effect needs will-change to feel smooth, cut the effect first.

CSS Image Effects FAQ

Does a CSS filter reduce image file size?

No. The browser downloads the full original file and applies the effect afterward, so a blurred or grayscale image costs the same bytes as the untouched one. To shrink the download, resize or compress the file itself.

What is the difference between filter: opacity() and the opacity property?

Both control transparency. filter: opacity() sits inside a filter chain, so its position among other functions changes the result, while the opacity property applies to the element on its own.

For a plain fade, the property is the simpler choice.

How do I apply a filter to a background image only?

Move the background onto a pseudo-element such as ::before, then put the filter on that pseudo-element. A filter on the parent would also blur or recolor the text and child elements sitting on top.

Position the pseudo-element behind the content with a lower z-index.

How do I make a circular image or round the corners?

Set border-radius on the img. A value of 50% turns a square image into a circle, and a fixed value such as 12px rounds the corners.

Add object-fit: cover so a non-square photo fills the shape without stretching. A border radius generator previews the values live.

How do I use filters in Tailwind CSS?

Tailwind ships a utility class per function: blur, brightness, contrast, drop-shadow, grayscale, hue-rotate, invert, saturate and sepia (Tailwind CSS documentation). Combine them on one element, add variants such as hover:, and use filter-none to reset.

Version 4 renamed blur to blur-sm and blur-sm to blur-xs.

Shipping CSS Image Effects in the Right Order

Release effects in order of support risk and rendering cost. That puts Widely Available properties ahead of newer ones and keeps expensive repaints out of the first release.

  • object-fit, clip-path and static filters
  • transform and opacity for hover motion
  • backdrop-filter and SVG filters behind @supports

The trade-off is that the showiest effects arrive last, in exchange for a page that falls back to the plain image in any engine.

The advice changes once backdrop-filter reaches Widely Available status, which moves it up the list (position checked October 2026).

Image-heavy layouts are the natural test bed, and building a CSS gallery gives each step in the sequence a real page to run on.