Revealing Images With CSS: 3D Frames Without Extra Markup

Creating 3D effects in CSS is commonly done by adding extra elements in the markup and using pseudo-elements in the styles. But what happens when you can’t modify the HTML? Temani Afif demonstrates a technique that achieves both a 3D frame and a sliding reveal effect using only a single <img> element — no wrapper <div> and no pseudo-elements.

See the Pen [Image gift box (hover to reveal)](https://codepen.io/smashingmag/pen/LYaPPPo) by Temani Afif.

See the Pen Image gift box (hover to reveal) by Temani Afif.

The effect looks like an overlay sliding up and away to reveal an image behind a border. But there is no overlay element. Instead, the illusion is built using padding, background colors, and clipped gradients.

Simulating the Overlay

Since the markup is limited to one image tag, you can’t add a real overlay element. The trick is to fake it by letting the element’s background function as the cover.

Starting with some basic CSS:

img {
  --s: 200px; /* the image size */
  
  width: var(--s);
  box-sizing: border-box;
  padding-right: var(--s);
  background: #8A9B0F;
  transition: 1s;
}
img:hover {
  padding: 0;
}

In this setup, width is assigned to a CSS variable, --s, and the padding is set to the same value along the right side. With box-sizing: border-box, this makes the content box width zero. So the image itself is fully hidden and the visible area is entirely the element’s padding, which shows the background color.

Transitioning the padding to zero on hover widens the content box and gradually reveals the image:

See the Pen [Padding animation to reveal the image](https://codepen.io/smashingmag/pen/jOJrqXo) by Temani Afif.

See the Pen Padding animation to reveal the image by Temani Afif.

At this point the image stretches as the box expands, because the image fills the content box. To keep its natural aspect ratio, object-fit: cover is applied, and object-position: left locks the left side of the image in place while the content box grows:

img {
  object-fit: cover;
  object-position: left;
}

See the Pen [Adding object-* properties](https://codepen.io/smashingmag/pen/oNVLLdM) by Temani Afif.

See the Pen Adding object-* properties by Temani Afif.

Now the image stays in one spot and expands into view, which looks exactly like an overlay is being swept away — even though it’s just the image itself becoming visible.

Changing object-position to right cycles the effect into a different animation where the image appears to slide in from the left:

See the Pen [Using object-position: right](https://codepen.io/smashingmag/pen/xxBOOJW) by Temani Afif.

See the Pen Using object-position: right by Temani Afif.

See the Pen [Fancy Pop Out Reveal hover effect!](https://codepen.io/smashingmag/pen/VwqWRyj) by Temani Afif.

See the Pen Fancy Pop Out Reveal hover effect! by Temani Afif.

A small demo shows all the variations and how easy it is to toggle between them with just a few CSS values:

See the Pen [A reveal hover effect with one element](https://codepen.io/smashingmag/pen/GRYEZrr) by Temani Afif.

See the Pen A reveal hover effect with one element by Temani Afif.

Moving the Overlay Off the Image

Another route is to let the “overlay” move completely outside the image’s bounds. This is done by keeping the size constant and animating a clipped box-shadow:

img {
  --s: 200px; /* the image size */

  box-shadow: 0 0 #8A9B0F; 
}
img:hover {
  box-shadow: var(--s) 0 #8A9B0F;
}

See the Pen [Adding box-shadow animation](https://codepen.io/smashingmag/pen/KKEMvNq) by Temani Afif.

See the Pen Adding box-shadow animation by Temani Afif.

Here, the box-shadow is inset and positioned so that increasing the inset values on hover reveals the shadow on only one side. The same effect can be produced using a clip-path approach:

img {
  --s: 200px; /* the image size */

  box-shadow: 0 0 0 999px #8A9B0F; 
  clip-path: inset(0 0 0 0);
}
img:hover {
  clip-path: inset(0 -100% 0 0);
}

See the Pen [Using clip-path instead of box-shadow](https://codepen.io/smashingmag/pen/YzgWxxK) by Temani Afif.

See the Pen Using clip-path instead of box-shadow by Temani Afif.

Any direction can be chosen by simply adjusting the inset or clip path values.

Adding Borders and Aligning Transitions

Adding a border complicates matters. The border overrides the background painted beneath it, and its thickness mixes with the padding to mess up the sizing. To straighten this out, the padding must be reduced to account for the border’s thickness, and the border needs to be transparent so the background color shows through:

See the Pen [Adding border](https://codepen.io/smashingmag/pen/YzgWrvQ) by Temani Afif.

See the Pen Adding border by Temani Afif.
img {
  --s: 200px; /* the image size */
  --b: 10px;  /* border width */
  --c: #8A9B0F;
  
  width: var(--s);
  aspect-ratio: 1;
  box-sizing: border-box;
  padding-top: calc(var(--s) - 2*var(--b));
  border: var(--b) solid #0000;
  box-shadow: 0 0 0 999px var(--c); 
  background: var(--c);
  clip-path: inset(0);
  object-fit: cover;
  object-position: bottom;
}
img:hover {
  padding: 0;
  clip-path: inset(-100% 0 0);
}

See the Pen [Fixing the border issue](https://codepen.io/smashingmag/pen/mdoEBQX) by Temani Afif.

See the Pen Fixing the border issue by Temani Afif.

To add distinction between the border and background areas, multiple backgrounds come into play. The element can have a base background color, plus a gradient layer that acts as the “cover” color:

img {
  --c: #8A9B0F;
  --_c: color-mix(in srgb, var(--c), #fff 25%);
  
  background:
    linear-gradient(var(--_c) 0 0) no-repeat
     0 0 / 100% 100%,
    var(--c);
  background-origin: border-box;
  box-shadow: 0 0 0 999px var(--_c);
  /* same as previous */
}
img:hover {
  background-size: 100% 0%;
  /* same as previous */
}

The color-mix() function creates a lighter tint of the primary color by mixing with white. This tint forms the gradient layer, and the box-shadow uses the same tint.

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

See the Pen Adding gradient animation by Temani Afif.

A subtle alignment problem arises, though: the padding and the gradient don’t transition at exactly the same rate, causing brief misalignment between the cover and the border:

Capturing the overlay mid-slide and highlighting where the border and overlay intersect
(Large preview)

The fix is to make the padding transition’s range match the gradient’s range:

img {
  --h: calc(var(--s) - var(--b));
  padding-top: min(var(--h), var(--s) - 2*var(--b));
  transition: --h 1s linear;
}
img:hover {
  --h: calc(-1 * var(--b));
}

A new variable --h runs the same path as the background, using min() to clamp the padding at zero when it goes negative. A clamp() declaration can be used instead of min(), but it’s not strictly necessary since padding clamps to 0 on its own:

padding-top: clamp(0px, var(--h), var(--s) - 2*var(--b));

See the Pen [Fixing the padding issue](https://codepen.io/smashingmag/pen/ExMgZbW) by Temani Afif.

See the Pen Fixing the padding issue by Temani Afif.

Note that transitions on custom properties require @property, and at the time of writing, that transition doesn’t work in Firefox.

Building the 3D Frame

For the 3D look, strip away the overlays, gradients, and shadows for the moment and examine the skeletal box:

See the Pen [The revealed image with border](https://codepen.io/smashingmag/pen/QWoKpyE) by Temani Afif.

See the Pen The revealed image with border by Temani Afif.

The 3D effect emerges in three steps:

  • Thickening the left and bottom borders to suggest depth
  • Adding a conic gradient for shaded faces
  • Clipping the corners to produce the box form

First, increase the border’s thickness on the left and bottom:

img {
  --b: 10px; /* the image border */
  --d: 30px; /* the depth */

  border: solid #0000;
  border-width: var(--b) var(--b) calc(var(--b) + var(--d)) calc(var(--b) + var(--d));
}

Next, place a conic gradient over the entire background. Blending dark semi-transparent black colors against the base background creates subtle variations of the main tint:

background: 
  conic-gradient(at left var(--d) bottom var(--d),
   #0000 25%,#0008 0 62.5%,#0004 0) 
  var(--c);

Finally, clip just the right portion of the gradient so it ends up looking like the shaded walls of a 3D box build:

clip-path: polygon(var(--d) 0, 100% 0, 100% calc(100% - var(--d)), calc(100% - var(--d)) 100%, 0 100%, 0 var(--d));

See the Pen [The image within a 3D box](https://codepen.io/smashingmag/pen/JjzRWXZ) by Temani Afif.

See the Pen The image within a 3D box by Temani Afif.

Bring the pieces back together. Reintroducing the padding already accounts for the new depth dimension — a new variable --d measures how much the bottom border gets boosted:

See the Pen [Putting back the padding animation](https://codepen.io/smashingmag/pen/ExMgWXR) by Temani Afif.

See the Pen Putting back the padding animation by Temani Afif.
--h: calc(var(--s) - var(--b) - var(--d));
padding-top: min(var(--h),var(--s) - 2*var(--b) - var(--d));

That changes the formula for how far the gradient must cover. Reducing its dimensions keeps the painting areas from overlapping:

See the Pen [Putting back the gradient animation](https://codepen.io/smashingmag/pen/VwRKpzN) by Temani Afif.

See the Pen Putting back the gradient animation by Temani Afif.

Finally, reintroduce the box-shadow, which cycles by polygon movement on hover. Two of its corner points shift inward and back, lifting a broadened panel away:

img {
  clip-path: polygon(
    var(--d) 0, /* --> var(--d) calc(-1*(var(--s) - var(--d))) */
    100%     0, /* --> 100%     calc(-1*(var(--s) - var(--d))) */
    
    /* the fixed points */ 
    100% calc(100% - var(--d)), /* 1 */
    calc(100% - var(--d)) 100%, /* 2 */
    0 100%,                     /* 3 */
    0 var(--d),                 /* 4 */
    var(--d) 0);                /* 5 */
}

With that, the entire 3D cover sequence works within neither a wrapper element nor pseudo-elements. The final structure lets the image rise into a visible, framed region when the cover slides up:

See the Pen [3D image with reveal effect](https://codepen.io/smashingmag/pen/GRejXMK) by Temani Afif.

See the Pen 3D image with reveal effect by Temani Afif.

An optimized example simplifies the math, so only one value must be toggled on hover:

See the Pen [Image gift box (hover to reveal)](https://codepen.io/smashingmag/pen/LYaPPPo) by Temani Afif.

See the Pen Image gift box (hover to reveal) by Temani Afif.

Alternate 3D Configuration

Another possible perspective is to produce a split reveal effect — two halves of the cover pull apart rather than one panel sliding.

See the Pen [Image gift box II (hover to reveal)](https://codepen.io/smashingmag/pen/yLwBVGQ) by Temani Afif.

See the Pen Image gift box II (hover to reveal) by Temani Afif.

By adjusting padding, object-* positioning, and clip paths together, the depth variations multiply. The same underlying techniques shift roles just enough to create a completely new look.

Why Restrict Yourself to One Element?

Using an extra container or pseudo-elements would unquestionably be simpler. Choosing the constrained case — a single image tag, as when you can’t change your template — forces more out of modern CSS. It reveals that ideas like pixel-perfect padding-to-gradient coordination, strategic clipping, and delegated color mixing can all compound to recreate effects typically considered out of reach without markup shims.

View the resulting effects less as an exercise in constraint and more as a survey of how flexible the CSS presentation layer has become.

Smashing Editorial