Masked Gradients as Hover Physics

CSS image effects often need extra markup, pseudo-elements, or a mix of both. The experiments here do neither. Using a single <img> element and CSS masks paired with gradient backgrounds, we can build hover transitions that wipe away a frame or crop an image into slices — all without touching the document structure.

The technique relies on one core idea: the <img> itself doubles as the container. Padding on the image lets a gradient background show through, and a CSS mask hides everything except that padded frame. Then, changing the mask’s shape, position, or size on hover reveals the image beneath. The result is a reveal animation driven entirely by CSS variables and composited gradients.

The markup stays identical in every demo:

<img src="" alt="">

The same pattern repeats across the examples: gradient masks layered onto the image, combined with mask-composite, then animated by altering the mask’s geometry. The code below uses standard CSS properties; browser support for mask-composite is inconsistent, so prefixed versions are included in the live demos for broader coverage.

Hole-Punch Reveal

The first effect starts with a square image framed by a gradient border. The frame is produced by a repeating linear gradient applied as the <img> background, with padding exposing it. Two mask layers then isolate the frame:

img {
  /* etc. */
  mask:
    linear-gradient(#000 0 0) padding-box,
    linear-gradient(#000 0 0) content-box;
}

The content-box mask maps to the image’s content area; the padding-box mask spans the full padded area. Compositing with exclude removes the overlap, leaving only the frame visible. The two layers and their compositing rule:

img {
  /* etc. */
  mask:
    linear-gradient(#000 0 0) padding-box,
    linear-gradient(#000 0 0) content-box;
  mask-composite: exclude;
}

That produces a visible gradient frame around a hidden image. Alternatives exist: a conic-gradient mask can punch the same hole without mask-composite. But compositing avoids hardcoding the padding value, so it is the approach I prefer.

Replacing the content-area mask with a radial-gradient creates a circular cutout:

See the Pen [Adding the radial-gradient](https://codepen.io/smashingmag/pen/wvQOJmb) by Temani Afif.

See the Pen Adding the radial-gradient by Temani Afif.

The exclude value cuts a hole in the image. Scaling the radius up shrinks the visible area; shrinking the radius expands it. Setting the mask size to 141% fully covers the image, so the hover state can transition that value down to zero for a wipe-in effect. The complete CSS for the hover:

img {
  padding: 10px; /* control the thickness of the gradient "border" */
  background: repeating-linear-gradient(45deg, #FF6B6B 0 10px, #4ECDC4 0 20px);
  mask:
    linear-gradient(#000 0 0),
    radial-gradient(#000 70%, #0000 71%) content-box
      50% / var(--_s, 150% 150%) no-repeat;
  mask-composite: exclude;
  transition: .5s;
}
img:hover {
  --_s: 0% 0%;
}

That compresses to a few rules: a CSS variable holds the initial mask size, hover reduces it, and a .5s transition smooths the change. Because the mask position can be shifted as easily as the size, a variation of the same effect fades in from a different corner:

See the Pen [Another variation of the circular reveal animation](https://codepen.io/smashingmag/pen/dyQrvEq) by Temani Afif.

See the Pen Another variation of the circular reveal animation by Temani Afif.

Swiping Gradients

A second effect uses a linear gradient instead of a radial one. The size becomes 200% 200%, and hover moves the mask position from 0% 0% to 100% 100% instead of changing its dimensions:

img {
  padding: 10px; /* control the thickness of the gradient "border" */
  background: repeating-linear-gradient(45deg, #FF6B6B 0 10px, #4ECDC4 0 20px);
  mask:
    linear-gradient(#000 0 0),
    linear-gradient(135deg, #000 50%, #0000 0) content-box 
      0% 0% / 200% 200% no-repeat;
  mask-composite: exclude;
  transition: .5s;
}
img:hover {
  mask-position: 100% 100%;
    }

Reversing the gradient angle to -45deg and swapping the hover position inverts the swipe direction:

img {
  padding: 10px; /* control the thickness of the gradient "border" */
  background: repeating-linear-gradient(45deg, #FF6B6B 0 10px, #4ECDC4 0 20px);
  mask:
    linear-gradient(#000 0 0),
    linear-gradient(-45deg, #000 50%, #0000 0) content-box 
      100% 100% / 200% 200% no-repeat;
  mask-composite: exclude;
  transition: .5s;
}
img:hover {
  mask-position: 0% 0%;
}

Only one position value is declared for two mask layers. Since the first gradient occupies its full area, percentage-based positions cannot move it; that value only affects the second layer.

Combining the two swipe directions produces an effect where both gradients start at the center (50% 50%) — one covering the top-left, the other the bottom-right. Hover slides both outward. The three-layer mask introduces a complication, though. Simply stacking the gradients leaves a faint diagonal seam from anti-aliasing:

img {
  padding: 10px; /* control the thickness of the gradient "border" */
  background: repeating-linear-gradient(45deg, #FF6B6B 0 10px, #4ECDC4 0 20px);
  mask:
    linear-gradient(#000 0 0),
    linear-gradient(135deg, #000 50%, #0000 0) content-box 
      50% 50% / 200% 200% no-repeat,
    linear-gradient(-45deg, #000 50%, #0000 0) content-box 
      50% 50 / 200% 200% no-repeat;
  mask-composite: exclude;
  transition: .5s;
  cursor: pointer;
}
img:hover {
  mask-position: 0% 0%, 100% 100%;
}

Adjusting the gradient stop from 50% to 50.5% makes the artifact worse. The fix is to change how the mask layers composite. Declaring exclude on the first layer alone isn’t enough; the second and third layers must first be combined with add before being excluded:

img {
  mask:
    /* 1st layer */
    linear-gradient(#000 0 0),

    /* 2nd layer */
    linear-gradient(135deg, #000 50.5%, #0000 0) content-box 
      50% 50% / 200% 200% no-repeat,

    /* 3rd layer */
    linear-gradient(-45deg, #000 50.5%, #0000 0) content-box 
      50% 50% / 200% 200% no-repeat;

  mask-composite: exclude, add;
}

This eliminates the seam. A further subtlety: the hover state declares two mask-position values, but the effect uses three mask layers. CSS repeats the value list to cover the missing layer, so the first value also applies to the third gradient.

Reverse-Engineering the Zig-Zag

A third demo shows zig-zag edges sliding apart over the image. The effect feels more complex, but it depends on the same physics discussed above — gradient shapes as masks plus layer compositing. The full implementation is left as an exercise, though two resources explain how to build the sawtooth edge: a mask-based border technique and a generator for custom borders.

Gradients as Cutout Stencils

These experiments demystify a confusing part of CSS. Masks behave like stencils: gradients taught us to think of them as cutout templates, and compositing tells the browser how overlapping stencils interact. Animating a reveal is then only a matter of moving or scaling one layer’s geometry, all within the bounds of a single image tag.

See the Pen [Hover reveal animation using mask IV](https://codepen.io/smashingmag/pen/PoxLMOy) by Temani Afif.

See the Pen Hover reveal animation using mask IV by Temani Afif.

See the Pen [Hover reveal animation using mask V](https://codepen.io/smashingmag/pen/jOQJgxN) by Temani Afif.

See the Pen Hover reveal animation using mask V by Temani Afif.

See the Pen [Hover reveal animation using mask VI](https://codepen.io/smashingmag/pen/gOQEVdQ) by Temani Afif.

See the Pen Hover reveal animation using mask VI by Temani Afif.