CSS & HTML

CSS Image Styles: Rounded, Masked, Filtered and Blended

By Aigars Silkalns Updated 8 min read

CSS Image Styles: Rounded, Masked, Filtered and Blended

You can style a plain <img> in modern CSS without wrapping it in extra markup or swapping it for a background image. Use border-radius for rounded corners, object-fit and aspect-ratio for consistent crops, mask-image and clip-path for custom shapes, and filter or mix-blend-mode for color treatments. A pseudo-element on a wrapper handles the one thing the image itself cannot do: draw an inset shadow or ring on top of the photo.

This article replaces an older tutorial that used jQuery to turn every image into a background-image span, because browsers of that era ignored border-radius and inset shadows on images. That workaround is no longer needed. Below are eight techniques that produce nine looks (the card ring and the embossed edge share one technique), each with the CSS to build it, all tested in a single demo page. A complete copy-paste version of the demo is at the end.

Nine styled copies of one landscape image: circle, card ring, embossed, feathered mask, hexagon, arch, grayscale, duotone and hover zoom
One image, nine looks, all plain CSS on a standard img element.

What changed since the background-image trick

The old advice was to wrap images in a span and paint the picture as a background. Today the image element does most of the work itself, and what remains has a clean answer.

Goal Old workaround Now
Rounded corners, circles jQuery wrapper plus background-image border-radius on the img
Square crop of any photo Fixed width and height, or a cropped copy aspect-ratio plus object-fit: cover
Inset ring or emboss Inset box-shadow on the span Inset box-shadow on a ::after overlay
Feathered edge White radial gradient laid over the image mask-image with a radial gradient
Custom shapes Image sprites, PNG masks clip-path: polygon() or border-radius shapes
Duotone, grayscale Edited copies in Photoshop filter and mix-blend-mode

The shared base: crop with object-fit and aspect-ratio

Every style below starts from the same base. Give the image a ratio and let it fill the box without distortion.

.frame img {
  display: block;
  width: 100%;
  aspect-ratio: 1;      /* or 4 / 3, 16 / 9, 3 / 4 */
  object-fit: cover;    /* crop instead of stretch */
}

With object-fit: cover the browser crops the image to fill the box, and object-position moves the crop point if a face or subject sits off center. Always keep real width and height attributes in the HTML as well so the browser reserves space before the file loads and the page does not shift.

1. Circle and rounded corners

Applying border-radius to an image works in every current browser, so the jQuery wrapper from 2011 can go.

.circle img { border-radius: 50%; }
.rounded img { border-radius: 20px; }

A circle needs a square box, which is why the base sets aspect-ratio: 1. On a non-square box 50% gives you an ellipse. If you want a pill-shaped crop instead, use a very large value such as 999px.

2. Card ring and embossed edges with a pseudo-element

An inset box-shadow still does not show on an img itself, because the shadow is painted beneath the replaced content and the photo covers it. The fix is to draw the shadow on an overlay that sits above the image. Put the image in a wrapper and use ::after.

<div class="frame card">
  <img src="photo.jpg" width="800" height="600" alt="Mountains at sunset">
</div>
.card {
  position: relative;
  border-radius: 20px;
  overflow: hidden;
}
.card::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;   /* clicks go through to the image or link */
  box-shadow:
    inset 0 0 1px rgb(0 0 0 / 0.8),
    inset 0 2px 0 rgb(255 255 255 / 0.5),
    inset 0 -1px 0 rgb(0 0 0 / 0.4);
}

For the embossed variant, keep the same wrapper and stack a thicker bottom edge, like a raised button:

.emboss::after {
  box-shadow:
    inset 0 0 2px rgb(0 0 0 / 0.8),
    inset 0 2px 0 rgb(255 255 255 / 0.5),
    inset 0 -7px 0 rgb(0 0 0 / 0.6),
    inset 0 -9px 0 rgb(255 255 255 / 0.3);
}

The unprefixed inset shorthand and the space-separated rgb() syntax with a slash for alpha are both supported everywhere current. Keep pointer-events: none on the overlay, otherwise it swallows clicks and hovers meant for the image or its link.

3. Feathered edge with mask-image

The old version laid a white gradient over the photo, which only worked on a white page. A mask actually removes pixels, so the image fades into whatever is behind it, including a dark theme or a colored section.

.feather img {
  mask-image: radial-gradient(circle, #000 45%, transparent 70%);
}

Black areas of the mask are visible and transparent areas are hidden. Move the two percentages closer together for a harder edge or farther apart for a softer fade. Unprefixed mask-image is supported in current Chrome, Edge, Safari and Firefox (see the MDN compatibility table), so no -webkit- fallback is needed for new projects.

4. Hexagon and other shapes with clip-path

clip-path cuts the element to any polygon, circle or path, and is supported in all current browsers (MDN reference). A hexagon is six percentage points:

.hex img {
  clip-path: polygon(25% 5%, 75% 5%, 100% 50%, 75% 95%, 25% 95%, 0 50%);
}

Percentages scale with the image, so the shape stays proportional on every screen. Note that a clipped region also clips shadows and outlines. If you need a drop shadow on a clipped image, apply filter: drop-shadow() to the wrapper rather than box-shadow to the image.

5. Arch shape with border-radius alone

Not every shape needs clip-path. Different radii per corner give the popular arch crop used in portrait and hero images.

.arch img {
  aspect-ratio: 3 / 4;
  border-radius: 999px 999px 12px 12px;
}

6. Grayscale that reveals color on hover

CSS filter replaces edited copies of each photo. A grayscale gallery that comes alive on hover takes four lines:

.mono img {
  filter: grayscale(1) contrast(1.1);
  transition: filter 0.4s;
}
.mono:hover img,
.mono:focus-within img {
  filter: none;
}

Include :focus-within so keyboard users who tab to a linked image get the same reveal as mouse users. Our guide to 9 simple CSS image filters covers the other filter functions in detail.

7. Duotone with mix-blend-mode

A duotone maps shadows to one color and highlights to another. In CSS you can approximate it by turning the image grayscale and multiplying it with a colored background.

.duo {
  background: oklch(0.72 0.17 25);   /* highlight color */
  border-radius: 16px;
  overflow: hidden;
  isolation: isolate;                /* own stacking context */
}
.duo img {
  filter: grayscale(1) brightness(1.5) contrast(1.1);
  mix-blend-mode: multiply;
}

The wrapper sets isolation: isolate so it forms its own stacking context. Without it, mix-blend-mode can blend the image with whatever page content sits behind the wrapper instead of just its background color.

Multiply darkens, so a dark source image gives a very dark result. The brightness() value compensates, and you will need to tune it per photo. Change the background color to retheme every duotone image on the site at once. For a deeper look at blend modes, see how to create Spotify colorizer effects with CSS blend modes.

8. Hover zoom without layout shift

Scale the image inside a clipping wrapper so the box itself never changes size.

.zoom {
  overflow: hidden;
  border-radius: 16px;
}
.zoom img { transition: scale 0.5s; }
.zoom:hover img { scale: 1.1; }

@media (prefers-reduced-motion: reduce) {
  .zoom img { transition: none; }
}

The standalone scale property replaces transform: scale() when you only need scaling, and it lets you animate it without rewriting a full transform list. The reduced-motion query respects people who have asked their system to limit animation. For a scroll-based variant, read how to zoom hero images on scroll.

Accessibility and performance notes

  • Keep images as images. An img with real alt text is accessible and indexable. A background image is neither, which was the hidden cost of the old workaround.
  • Decorative overlays need no markup. Rings and emboss effects live in ::after, so screen readers never see them.
  • Never rely on a filter for meaning. If grayscale hides a state (for example, sold out), add text as well.
  • Test contrast over images. If text sits on a duotone or masked photo, check it against the lightest area of the result.
  • Animate cheap properties. scale, opacity and filter transitions are smooth for a handful of images, but heavy filters on dozens of large photos can stutter on low-end phones.

Full demo markup

To reproduce the nine-tile page, save this as one HTML file and put any landscape photo next to it as photo.jpg. It is the same CSS as above, combined.

<!doctype html>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Image styles</title>
<style>
  * { box-sizing: border-box; }
  body { margin: 0; padding: 2rem; font: 15px/1.4 system-ui, sans-serif; }
  .grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.5rem; max-width: 900px; margin: auto; align-items: start; }
  figure { margin: 0; }
  figcaption { margin-top: .5rem; font-weight: 600; text-align: center; }
  .frame img { display: block; width: 100%; aspect-ratio: 1; object-fit: cover; }

  .circle img { border-radius: 50%; }
  .card { position: relative; border-radius: 20px; overflow: hidden; }
  .card::after { content: ""; position: absolute; inset: 0; border-radius: inherit; pointer-events: none;
    box-shadow: inset 0 0 1px rgb(0 0 0 / .8), inset 0 2px 0 rgb(255 255 255 / .5), inset 0 -1px 0 rgb(0 0 0 / .4); }
  .emboss::after { box-shadow: inset 0 0 2px rgb(0 0 0 / .8), inset 0 2px 0 rgb(255 255 255 / .5),
    inset 0 -7px 0 rgb(0 0 0 / .6), inset 0 -9px 0 rgb(255 255 255 / .3); }
  .feather img { mask-image: radial-gradient(circle, #000 45%, transparent 70%); }
  .hex img { clip-path: polygon(25% 5%, 75% 5%, 100% 50%, 75% 95%, 25% 95%, 0 50%); }
  .arch img { aspect-ratio: 3 / 4; border-radius: 999px 999px 12px 12px; }
  .mono img { filter: grayscale(1) contrast(1.1); transition: filter .4s; }
  .mono:hover img, .mono:focus-within img { filter: none; }
  .duo { background: oklch(0.72 0.17 25); border-radius: 16px; overflow: hidden; isolation: isolate; }
  .duo img { filter: grayscale(1) brightness(1.5) contrast(1.1); mix-blend-mode: multiply; }
  .zoom { overflow: hidden; border-radius: 16px; }
  .zoom img { transition: scale .5s; }
  .zoom:hover img { scale: 1.1; }
  @media (prefers-reduced-motion: reduce) { * { transition: none !important; } }
</style>
<div class="grid">
  <figure><div class="frame circle"><img src="photo.jpg" width="800" height="600" alt=""></div><figcaption>Circle</figcaption></figure>
  <figure><div class="frame card"><img src="photo.jpg" width="800" height="600" alt=""></div><figcaption>Card ring</figcaption></figure>
  <figure><div class="frame card emboss"><img src="photo.jpg" width="800" height="600" alt=""></div><figcaption>Embossed</figcaption></figure>
  <figure><div class="frame feather"><img src="photo.jpg" width="800" height="600" alt=""></div><figcaption>Feathered mask</figcaption></figure>
  <figure><div class="frame hex"><img src="photo.jpg" width="800" height="600" alt=""></div><figcaption>Hexagon</figcaption></figure>
  <figure><div class="frame arch"><img src="photo.jpg" width="800" height="600" alt=""></div><figcaption>Arch</figcaption></figure>
  <figure><div class="frame mono"><img src="photo.jpg" width="800" height="600" alt=""></div><figcaption>Grayscale</figcaption></figure>
  <figure><div class="frame duo"><img src="photo.jpg" width="800" height="600" alt=""></div><figcaption>Duotone</figcaption></figure>
  <figure><div class="frame zoom"><img src="photo.jpg" width="800" height="600" alt=""></div><figcaption>Hover zoom</figcaption></figure>
</div>

Where to go next

If you want the history, the original technique is still in CSS3 rounded image with jQuery. For more shapes and layouts, see Decorative CSS Gallery, Part 2, and for ready-made hover treatments in a card grid, try the expanding image gallery on hover and the category image cards with zoom hover snippets.

67 comments from the archive
  1. I’ve done a bunch of testing and border-radius on images seems to work rather well currently without any extra finicky work. Or, at least, in my experience if their absolutely positioned it does. I haven’t done an exhaustive study and it was just WebKit (Safari/Chrome) and Firefox on the Mac. However, I would expect to find a similar behavior on Windows.

    Likewise, with inset shadow if you position the image relative (or absolute, for that matter) you can use the :before pseudo-selector to create an absolutely positioned box with a width and height of 100% (goes just edge to edge of the IMG) and give that an inset shadow as well as a z-index to lift it above the image. Works the same as border radius.

    1. Currently image elements do not support pseudo-elements, so unfortunately the second suggestion will not work.

  2. Awesome! Thanks a ton for explaining the “make it a background image” fix. Can’t tell you how many hours i had wasted trying to apply some CSS3 directly to the img.

  3. ! :D Thank you, I love it! This is just the issue I have been grappling with on my portfolio, I have tested your code and styles out and it looks darn swell. Now I am fast at work writing it in. Thanks again so much!

  4. I have problems with the jQuery Code.
    I’m using Drupal, so i added the code and cleared Drupal cache. On the first time I load the page, die images are wrapped correctly and everything looks nice. When I hit the reload button, the images are not wrapped anymore. It seems, that Safari & Chrome (OSX) caches the images and reloading the page doesn’t trigger the jQuery .load() function. I don’t have a solution yet, but for me it is not working in all situations/browsers. I even had the problem, that jQuery assumes a width & height of 0 for these cached images, so I added if($(this).width != 0){ … } to the code. In iOS Safari and Firefox (OSX) it is working correctly.

    1. https://api.jquery.com/load-event/

      It doesn’t work consistently nor reliably cross-browser
      It doesn’t fire correctly in WebKit if the image src is set to the same src as before
      It doesn’t correctly bubble up the DOM tree
      Can cease to fire for images that already live in the browser’s cache

      So there are known issues with the img .load() function vs. browser caching. The annoying thing is, that is works on the first page load. When a user reloads the effect is gone – that really destroys User Experience. Maybe someone finds a solution here…

  5. Awesome post!!! All the effects one might need in one ;)
    YET, seems like Opera doesn’t render them properly….
    I don’t see how, since it’s codded for all renderers…
    :?

  6. Just a heads up, Chrome and Safari do render the box-shadow, it just sits behind the image (which is as good as not rendering it at all). They both render border-radius correctly.

  7. Gotta give you wraps on this, some really great work. Your code will come in very handy, hope the credit I pass on will also. Cheers

  8. Thanks for sharing such a great work. I’ve bookmarked this post and surely I’m gonna read it serveral times in the future.

  9. Strange to see something that works default on IE10 (maybe IE9 too, haven’t tested), but fails on Firefox/Webkit. It used to be the other way around :)

  10. Those are awesome tricks. I’ll definitely be experimenting with the ideas here.
    One question: whaddya do when the user has js turned off?

  11. What about IE 9? Nothing works, couple reloads do fix the border radis but antoher reload and things are gona again. In FF & Chrome everythings works fine.

  12. Wow! This is really nice! No photoshop needed to do lots os cool stuffs. Tks!

  13. Great, you just covered almost every part of every effect one can apply to images using CSS3. This article worth that much that I actually bookmarked it for future reference.

    I don’t know why Microsoft doesn’t get along the standard path to implement these cool stuff.

    Just one note, and that is, I think Safari supports real reflections. I don’t know for sure, though.

  14. I try to see the examples on ie (Internet Explorer) but all is bad, how can i get a script to see this example in ie? Thanks

    Hola, he intentado ver los ejemplos en internet explorer y todo a salido fatal, no se ven como deben ser. ¿Hay algun script que permita ver los efectos en Internet Explorer?

  15. This is a great write-up. I’ve been working on image styling for mobile websites and I really appreciate the detail you go into here.

  16. Thank you so much for that!!! It’s exactly what I needed and you saved me hours of work!!!!

  17. Good tutorial, so I learned a lot techniques. I really like your tutorial, I hope you can always give us such a good tutorial. I intend to put this translation into Chinese, into my blog. Well, thank you very much.

  18. So much to digest. Is there a way to print it off the screen, with all the coding which is showing horizontal scroll bars?

  19. WordPress Blogs have become a staple in website development and absolutely needed for content rich and ever growing sites. Theme’s and theme development is an excellent way to gain control of your WP integration.

  20. great article! but do you know if google images shows images that were set in background?

  21. Definitely the best resource i’ve found for CSS3 image manipulation. Thanks!

  22. Hi, awesome tutorial, you really show what can be done with css3.
    I have a question, I’m working with templates, so can’t change html, and I’m trying to apply some tinting to a JPG images, but the inner shadow does not come over the picture, any idea how to make it straight to the picture?

    Thanks

  23. This is great stuff; very inspirational—thank you!

    I’m going to be nit-picky here, but if you want the reflection w/ mouse over example to be more realistic, the reflection should stay put while the main image rises up. Just a thought. Nice work though. Kudos.

  24. Definitely using this in my new project. Something with a sleek clean look.

  25. I love your DEMO section which make easy to follow. I just can’t stop coming back to your site to learn web design stuff.

  26. This tutorial helped me in so much different projects !! you can’t imagine !

  27. By the css code, reduce the code & time. Also get best functionality.

  28. Greetings from wonderful Greece (irony)
    I tried to put it in a wordpress theme, it works perfect in firefox but it take effect (on safari and opera) only after a refresh. Does anyone know why that happens? does it have anything to do with the fact tha it it is still hosted on wamp test server?

  29. That is a very cool trick. Going to use it for mobile development since I’m building with PhoneGap, html5, css3 and js. Thanks!

  30. I love that iPhone style app image. I am going to use that somewhere in the future. BTW, great tutorial ;) Straight in my bookmarks bar!

  31. I used these css techniques to create a jquery Lavalamp menu with sons of suckerfish dropdown menus. They results were fabulous. Thanks for the tut!

  32. how can embed this css to dynamic php code?
    in code of table generator in catalog with i++

  33. one of the best article which ever read.. really superb.. CSS3 will be sure more than flash.. really appreciate the work

  34. What about enclosed divs?
    I have a div which enclosed 2 divs .. I have applied the rounded corners to the parent div but the child divs don’t get enclosed (they overlap) within the borders..

  35. Star Web Maker is a best company of improve your website ranking , eCommerce, software development,Web maintenance Services, Internet Marketing, web design services and Website Development etc visit starwebmaker.com

  36. Can some body please help me telling how to use the code.
    I inserted the css in my stylesheet but what to do next.
    Which code is to be placed and where to be placed after that.

  37. As the admin of this site is working, no doubt very rapidly it will be well-known, due to its quality contents.

  38. I have found, that css in form “-webkit-box-shadow: inset 0 0 2px rgba(0,0,0,.8)” (and other browser’s version) slows down Twitter Bootstrap tooltip in Opera (to see what ‘tooltip’ is see schmutzka.eu and hover some link).

    Solution: use classic #FFF, instead of rgba(0,0,0,1). No transparency, but no lags as well.

    Take me quite a while to discover this hidden connection :).

  39. I’m gone to convey my little brother, that he should also visit this weblog on regular basis to obtain updated from most recent information.

  40. I’m gone to convey my little brother, that he should also visit this weblog on regular

  41. Hi & Thanks NickLa, love ♥‿♥ your css tutorials, really helps me to tweaks my blog widgets.Wondering if you’ve had any googleplus page,just to follow your blog? ✿◕‿◕✿

  42. sorry – all sorted – the problems were in my implementation of jQuery – thank you very much for these insights…
    Paul

  43. Great collection of CSS effects. Some of them are very useful like the tape one.

Comments are closed on this article.