How to properly add a vignette to an image with CSS

February 24, 2015

I’ve noticed that a lot of pages use vignetting as a means to separate images from the rest of the page.

In photography and optics, vignetting (/vɪnˈjɛtɪŋ/; French: “vignette”) is a reduction of an image’s brightness or saturation at the periphery compared to the image center.
Wikipedia on vignetting

In other words, vignetting is actually an artifact from either the physical lens or the optics that darken the corners of the image. The way most people implement vignetting is by using the CSS box-shadow property with inset, this does the trick of darkening the sides of an image, but it’s not what vignetting would actually look like.

box-shadow vs vignette
On the left side is a normal box-shadow, on the right side is how a vignette would actually look like.

Most images are square, but a lens is round, in order to get the square format, the lens projects a round image that is larger than the film or sensor plane. Parts of the lens projection is cropped away (because the light doesn’t hit the recording medium).

vignette_projection
How an image is projected on the film or sensor plane.

In order to simulate this behaviour, box-shadow itself won’t cut it. Instead, we’ll have to use the pseudo-element :after, and since the img-tag doesn’t support it, we’re going to have to wrap it inside a div.

<div class="image-container">
    <img src="myimage.jpg" />
</div>

Now as the html-markup is done, we’ll look at the CSS. In order to achieve the effect, we’ll start off with adding an absolute positioned :after element with box-shadow on the container.

.image-container:after {
    content: '';
    position: absolute;
    top: 0%;
    left: 0%;
    width: 100%;
    height: 100%;
    box-shadow: inset 0px 0px 150px 60px rgba(0,0,0,0.8);
}

We’ve now basically achieved the normal box-shadow effect, so we’ll have to change the size of the pseudo-element so it’s formed as a circle, and a circle is just as wide as it’s high.

.image-container {
    position: relative;
}
.image-container:after {
    content: '';
    position: absolute;
    /* Center element on the middle of it's parent */
    top: 50%;
    left: 50%;
    /* Reset back the image so it's center is locked on the center of the parent */
    transform: translate(-50%,-50%);
    /* Only set the width of the image */
    width: 100%;
    /* Using the padding-force-ratio trick, we force the elments padding bottom to push down the height */
    padding-bottom: 100%;
    box-shadow: inset 0px 0px 150px 60px rgba(0,0,0,0.8);
    /* Make the element look round */
    border-radius: 50%;
}

At this point, the circle projection will be too small to cover the element, and it will lay above the image. So we’ll need to add about 20% more on the width (and padding-bottom) — to cover the image — the transform and padding-bottom trick will take care of the rest.

.image-container {
    position: relative;
    /* Remove the parts of the circle that is outside of the image */
    overflow:hidden;
}
.image-container:after {
    content: '';
    position: absolute;
    transform: translate(-50%,-50%);
    top: 50%;
    left: 50%;
    /* Add more width */
    width: 120%;
    /* To form a square, the padding-bottom, needs to have the same value as the width property */
    padding-bottom: 120%;
    box-shadow: inset 0px 0px 150px 60px rgba(0,0,0,0.8);
    border-radius: 50%;
}

Now the image should be properly vignetted (if that’s even a word). I’ve created a github gist for the code as well as Codepen example so you can test it out.

Tags