A Journey with Vignetting (That Doesn’t Get Very Far)

Avatar of Chris Coyier
Chris Coyier on (Updated on )

Ol’ Trent posted a quick tip post on using inset box-shadow for some simple yet classy effects. One of those techniques ended up with an vignette effect over top of an image. Like this:

Yeah, you could do it in Photoshop, but doing it through CSS means 1) it’s non-destructive 2) can be altered programmatically 3) is super consistent

When I first saw the example, I was like, hey that’s pretty cool, but why the wrapping div? Browsers allow box-shadow on <img> elements, so why not just use an inset box-shadow on it. That would be cleaner HTML.

<img src="amazinglandscape.jpg" alt="ooooh ahhhh" class="vignette">
.vignette {
	-webkit-box-shadow: inset 0px 0px 85px rgba(0,0,0,0.4);
	-moz-box-shadow:    inset 0px 0px 85px rgba(0,0,0,0.4);
	box-shadow:         inset 0px 0px 85px rgba(0,0,0,0.4);
}

Turns out, you can use inset box-shadows on images, it’s just the browser sets it behind the image, which mean if you use a fully opaque image (very likely if you are interested in vignetting it), you won’t see the shadow at all. To which Trent says:

While this seems pretty useless, it does make sense when you consider other kinds of content. For example, if you inset a shadow you probably wouldn’t want it overshadowing the text within.

I also agree with another point Trent makes: perhaps the official spec for box-shadow should be changed to say that when applied directly to an image the shadow should be on top. Alas, the spec nor the implementations do it this way.

Then I thought, hey wait a minute, let’s not resort to an wrapping element just yet. After all, keeping our HTML free of non-semantic wrappers is a noble goal. Why don’t we use a pseudo element on the image, which we can force to sit on top and be the exact size of the image, and put the inset box-shadow on that?

.vignette:after {
	-webkit-box-shadow: inset 0px 0px 85px rgba(0,0,0,0.4);
	-moz-box-shadow:    inset 0px 0px 85px rgba(0,0,0,0.4);
	box-shadow:         inset 0px 0px 85px rgba(0,0,0,0.4);

	position: absolute;
	top: 0; left: 0; bottom: 0; right: 0;
	content: "";
}

As I was working on a demo for this, it kept not working and confusing the heck out of me. For a minute I thought, I wonder if :before and :after pseudo elements just don’t work on inline elements*, but I proved they worked with spans and so that was out.

Then I heard from Nicholas Gallagher:

No elements with an ’empty’ Content Model support generated-content via pseudo-elements

By which Nicolas essentially means any element that doesn’t have innerHTML. Think:

<br />
<br />
<input />
<img />

This is apparently because the spec didn’t explain how to deal with elements of this type.

Much like the inset box-shadow thing, it kind of makes sense. You can think of a :before pseudo element as being within the tags but before other content, and an :after pseudo element as being with the tags but after other content.

<div>
   <!-- :before pseudo element -->
   Hi! I'm content!
   <!-- :after pseudo element -->
</div>

<img /> <!-- Where would pseudo content go? -->

For jQuery people, Tim Wright explains:

it’s that :after is more like jquery’s .append() than .after() so you can’t blop something in the middle of <img>

If you are thinking, dude, just put the pseudo elements AFTER or BEFORE the darn element, you aren’t alone. The problem is that’s not consistent with how it works with other elements and the spec doesn’t cover it. To make things a bit more complicated, that’s actually what Opera does.

So anyway… that’s it for ideas on how to do it cleanly. Let’s just use the wrapping div and get it over with:

<div class="vignette">
	<img src="handsomepeople.jpg" alt="ooooh ahhhh">
</div>
.vignette {
	-webkit-box-shadow: inset 0px 0px 85px rgba(0,0,0,0.4);
	-moz-box-shadow:    inset 0px 0px 85px rgba(0,0,0,0.4);
	box-shadow:         inset 0px 0px 85px rgba(0,0,0,0.4);
	
	line-height: 0;         /* ensure no space between bottom */
		
	display: inline-block;  /* don't go wider than image */
}
.vignette img {
	position: relative; 
	z-index: -1;            /* position beneath */
}

One possibly-desirable-possibly-annoying side effect? With the div overlay you can’t right-click-save the image or drag it to your desktop. Super primitive image protection.

View Demo

Related

  • Another technique by Dudley Storey which has you putting the image as a background-image of the vignetted element. Probably better for one-off’s but harder to use for sites where you just want to be able to use it wherever you want without needing to change CSS all the time.

* By the way, I was reading Introducing HTML5, and learned an interesting point. In HTML5, there really is no distinction between an “inline” element and an “block” element. All elements are inline until specified otherwise by a browser or user stylesheet.