Creating Shadows Around Polygons in CSS

Recently, clients have asked me on a couple of occasions to implement designs like this:

A tab-box with surrounding drop-shadows

When I say like this, I’m referring to the drop shadow around the whole component. Both the main box and the selected tab at the top have shadows, but there’s no overlap; they combine to form a single gradiated result. Effectively, they describe a non-rectangular polygon.

Tab boxes aren’t the only application for this kind of effect. I’ve seen it used in drop-down menus too, and I’m sure you can think of other examples. What the designer wants is the sense that the whole shape creates a drop shadow, as it would in nature.

So, how would you go about implementing this sort of effect? And would it surprise you if I told you that I did it with pure CSS, without using any images at all?

The lynchpin of it all is the area where the tab and the box combine. How do we get a shadow to describe that complex shape in CSS? Obviously it’s going to involve box-shadow (and its vendor equivalents), but that property can only create rectangular shapes. It is flexible to border-radius (and its vendor equivalents), so it can describe rounded corners rather than being limited to straight edges. But still, that caveat is not enough to describe the complex polygon that this design requires. So how is it done?

The answer is deceptively simple!

Almost every polygon can be reduced to a combination of simpler shapes—a reduction that’s the essence of how we make complex shapes with HTML and CSS in the first place. This example can be reduced down to two rectangles: the tab, and the main box.

So what we have to do is apply the shadow to both those boxes, and then cut off the overlap with clip!

Polygon Box-shadows

I’ve prepared a simplified test case to illustrate the technique, which looks superficially like the tab structure shown above, but is simply two boxes with solid background colors. Here’s the CSS for it:

#container
{
    position:relative;
}

#small
{
    background:#336;
    width:8em;
    height:3em;

    position:absolute;
    left:1em;
    top:0;
    z-index:100;
    clip:rect(-10px 9em 3em -10px);

    -moz-box-shadow:0 0 10px #002;
    -webkit-box-shadow:0 0 10px #002;
    box-shadow:0 0 10px #002;
}

#large
{
    background:#336;
    width:18em;
    height:20em;

    position:relative;
    top:3em;
    z-index:0;

    -moz-box-shadow:0 0 10px #002;
    -webkit-box-shadow:0 0 10px #002;
    box-shadow:0 0 10px #002;
}

Both the boxes have shadows, but the small one has a clip value that removes the shadow at its bottom edge. The top, right, and left clip values extend beyond the dimension of the element, giving its shadow room to spread; however, the bottom clip is the same as the element’s height, and this effectively cuts off any shadow that would extend below it.

The large rectangle’s shadow is allowed to spread freely, but the part of it that adjoins the small one is covered by that small one, because its z-index is higher, placing it above. Overall, the combination of positioning and clip removes hides the parts of the shadow we don’t want.

And that then creates the final effect we’re looking for: the appearance of pure CSS drop shadows around a non-rectangular shape!

Keen observers will note that I’ve used em-based dimensions and positioning, so that it expands with font size, and thereby counteracts one of the major problems that absolute positioning can otherwise give rise to. And since the container element has relative positioning, to create that positioning context the overall structure is still within the page flow, can still be moved by other page content, and is affected by window resizing.

And there you have it! This technique will work in all modern browsers, including Internet Explorer. Of course, IE is without box-shadow, but it does have a Glow filter, and that can create a reasonable enough effect if used subtly.

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • VP

    Add a dash before webkit, otherwise it won’t work.

  • Tom

    check webkit-box-shadow:0 0 10px #002; it doesnt have hyphen, those doesnt work in webkit browsers (tested in chrome)

  • http://www.pmob.co.uk Paul O’B

    Very useful tip James and nice to see clip being used as it seems to be almost a forgotten property.

  • http://www.brothercake.com/ brothercake

    Oops – webkit typo there! Thanks for the heads-up.

  • Greg

    I can add rounded corners and a border to make the example code more like the original image, but what is the correct way to deal with the border cutting across the join? And how about putting a reverse curve on at the join?

  • http://www.brothercake.com/ brothercake

    You can’t prevent the border cutting across the join, you just have to position the elements and design the borders to minimize that concurrence. If you look closely at the image, you can see that there is a slight overlap of borders where the tab’s bottom corners meet the larger box; but since it’s only 2px, and it’s minimized by the fact that the tab has no bottom-border (hence it’s rendered with a diagonal cut-off at 45deg to the corner), the effect is barely noticeable.

    If you want to see that actual example in the flesh, you need to join 99designs — it’s part of the buyer admin interface :)