Well Rounded: Compound Shapes in CSS

Avatar of Parker Bennett
Parker Bennett on (Updated on )

Learn Development at Frontend Masters

The following is a guest post by Parker Bennett. Parker is a bit of a regular around here, known for tackling common problems with unique solutions. This time he’s back at it creating complex shapes that have image backgrounds, shadows, and curves, yet are flexible.

I love to collaborate on design because it often pushes me to try new things. Recently, I was given a design comp something like this: a rounded compound shape serving as a fixed header, with a textured inset that had an inner shadow all around (a “well”). Well, I thought, how hard could this be?


One answer was super easy, of course: I could have opted to slice up an image and use transparent .png files for the curvy bottom – shadow and all – with an “end-cap” that would allow the right-half to resize responsively (the first column was designed to be fixed-width).

But I wanted more flexibility down the road, so I decided to tackle it using CSS. Here’s the approach I wound up taking, Nothing whiz-bangy, just good old CSS:

CodePen is Sassy!

CodePen turned out to be indispensable for all my experimentation, as well as for client review. One big advantage was being able to write in SCSS and quickly see the result: I could try different widths, colors, shadows, border radii, etc., just by changing variables.

I used variables for certain heights and widths, as well as box-shadow and border-radius sizes, then used those to calculate additional sizes – using ratios where it made sense to tie elements of the design together.

Variables rock!

In a Fix

For most projects I use a centered .page-wrap that’s a percentage of the browser window. But when you pin your header to the top using position: fixed, it’s no longer contained by the page-wrap. One solution is to start the page-wrap after the fixed header, and include a header-wrap that duplicates the width, margins, and padding of the page-wrap (details in this CodePen).

To maintain the illusion of the scrolling content being in a “cut-out” section below, I used overflow: hidden to crop the header’s drop shadow at the sides. This meant a second wrapper div, in order to also maintain a little padding between the content and the window frame at small widths (yes, I’m just that anal).

z-index: Leveling Up

Naturally, I wanted to keep the markup lean, and not depend on source order for the layout. I used pseudo elements where they seemed to make sense, and relied a lot on absolute positioning to put things in their place – using z-index to change how they overlapped. Because absolute positioning removes the element from the layout flow, I would need to compensate at times, for example, adding padding to other elements to fill that space.

As I worked out the positioning, I found I could paint myself into a z-index corner thanks to the peculiarities of stacking order: z-index requires positioning to work, un-positioned elements render first, and it’s easy to lose track of what elements are constrained by “stacking context” – for example, pseudo-elements can’t rise above the z-index of their parent (see demo on CodePen).

I also changed the z-index of shadows, making them a separate pseudo element. This let me tuck shadows behind objects where they needed to be obscured or, as on the sides of the .main scrolling content, raise them up to cover anything that reached the edge so as not break the illusion of depth.

/* elements that reach the edges of .main need to fall
   under edge shadow to maintain "cut-out" illusion */

.well-sides {
  /* child elements absolute */
  position: relative;
  overflow: hidden; }

.well-sides:before, .well-sides:after {
  content: "";
  display: block;
  position: absolute;
  /* higher than any z-index in .main, lower than header */
  z-index: 99;
  /* shadow size */
  top: -20px;
  height: 120%;
  /* shadow size */
  width: 20px;
  background: transparent; }

.well-sides:before {
  left: 0;
  /* negative spread */
  -webkit-box-shadow: inset 20px -20px 20px -20px rgba(0,0,0,0.35);
     -moz-box-shadow: inset 20px -20px 20px -20px rgba(0,0,0,0.35);
          box-shadow: inset 20px -20px 20px -20px rgba(0,0,0,0.35); }

.well-sides:after {
  right: 0;
  /* negative spread */
  -webkit-box-shadow: inset -20px 20px 20px -20px rgba(0,0,0,0.35);
     -moz-box-shadow: inset -20px 20px 20px -20px rgba(0,0,0,0.35);
          box-shadow: inset -20px 20px 20px -20px rgba(0,0,0,0.35); }

Cutting Corners

Unfortunately, there’s no practical way to create an object with a concave curve in CSS. Where the two rounded rectangles joined, I used a “patch” to cover the inset box-shadow “seam” and extend the textured background into the area of the inner corner. Then I overlapped it with a rounded white corner, creating the negative space as a positive. Finally, I added a normal box-shadow to the rounded corner (cropped by overflow: hidden) to blend it in with the inset box-shadow.

Outset shadow in “patch” had to be “opacified” a bit to match inset.
The inner circle

For the other inner curves I positioned a transparent square with one rounded corner and a thick white border on either side to complete the illusion. I used the border-radius and box-shadow sizes as variables to determine the size and placement of the corners.

How It All Stacks Up

Here’s how I put the pieces together:

Slideshow here. If reading through syndication, see the real blog post to see the slideshow.


  • Wrapping Up

    It’s true I like a good challenge, but working it out this way also gave me a lot of flexibility during the process, and a head start on a mobile responsive layout (straightening the curvy bottom at smaller sizes, truncating the menu, etc.). At any rate, I hope you found some useful bits in my way-too-thorough exploration. If you have any questions, comments, or corrections, drop me a line: parker@parkerbennett.com.