Shaded Progress Bars: A CSS/Sass Exercise

A tutorial on how to create some interesting 3D progress bars with CSS/Sass. Discover how to use box shadows and gradients to create realistic looking skins.

Today we’d like to show you how to create some pure CSS progress bars with a special 3D look. Consider this tutorial an advanced CSS exercise that will give you insight in a lot of interesting 3D properties and shading techniques. Creating UI components using only CSS will train your ability to think outside of the box and in this tutorial we will show you some tricks on how to create a more complex shape, use it as a progress bar and animate it.

Attention: Note that some of the CSS properties are only supported in modern browsers. IE still does not support transform-style: preserve-3d, a crucial property for creating nested 3D structures; so the progress bars will be flat/not work in IE.
Browser Support:
  • ChromeSupported
  • FirefoxSupported
  • Internet ExplorerNot supported
  • SafariSupported
  • OperaSupported

We’ll make some use of Sass (together with Compass) in this tutorial, so make sure to get it set up and understand the basics:

If you’d like to use a full-fledged solution for animated progress bars, you should check out ProgressBar.js by Kimmo Brunfeldt or PACE by HubSpot for excellent page load progress bars.

For generating all the necessary prefixes you can use something like Autoprefixer or the plugin for Sublime Text.

We’ll use a lot of interesting CSS properties like transform, perspective and box-shadow. We’ll also make extensive use of SASS for saving plenty of time in generating the bars’ positions and skins. By using relative sizes (em, percentages), we’ll make sure that our progress bars are easily resizable.

Building the Faces

Let’s start by building a box which will contain all the six faces. This box will work as our main container setting the sizes of our bar and its view point. We’ll also use a wrapper for the perspective, and this wrapper’s font-size property will allow is to scale the progress bar with the help of some em unit magic.

To make sure that all the faces are part of our 3D space, we need to apply transform-style: preserve-3d to the box.

So let’s start writing our styles by initiating some color variables:

$light-gray: #e0e0e0;
$magenta: #ec0071;
$white: #f5f5f5;

.perspective {
	font-size: 5em; // sets the main scale size
	perspective: 12em; // sets the perspective
	perspective-origin: 50% 50%;
	text-align: center;
}

.bar {
	display: inline-block;
	width: 1em;
	height: 1em;
	margin-top: 1em;
	position: relative;
	transform: rotateX(60deg); // sets the view point
	transform-style: preserve-3d; // perspective for the children
}

Now, let’s think about the faces. If we want to be able to rescale our main box without problems, the faces contained inside must have a liquid behavior and an absolute position.

.bar {
	// -> The SCSS written before
	.bar-face {
		display: inline-block;
		width: 100%;
		height: 100%;
		position: absolute;
		bottom: 0;
		left: 0;
		background-color: rgba($light-gray, .6); // just to see what is happening
	}
}

Let’s write the markup and make sure it’s accessible:

<div class="perspective">
	<div class="bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
		<div class="bar-face"></div>
		<div class="bar-face"></div>
		<div class="bar-face"></div>
		<div class="bar-face"></div>
		<div class="bar-face"></div>
		<div class="bar-face"></div>
	</div>
</div>
If you are interested to learn more about accessibility, this article (written by Gez Lemon) has helped me a lot: ‘Introduction to WAI ARIA’

Setting Up the Faces

This is a very important part. Our bar’s faces must be well oriented, so that we don’t get into trouble when we start adding the percentage fills.

.bar {
	// -> The SCSS from before
	.bar-face {
		// -> The SCSS from before
		transform-origin: 50% 100%;
		&.roof {
			transform: translateZ(1em);
		}
		&.front {
			transform: rotateX(-90deg);
		}
		&.right {
			left: auto;
			right: -.5em;
			width: 1em;
			transform: rotateX(-90deg) rotateY(90deg) translateX(.5em);
		}
		&.back {
			transform: rotateX(-90deg) rotateY(0deg) translateZ(-1em);
		}
		&.left {
			width: 1em;
			transform: rotateX(-90deg)rotateY(-90deg) translateX(-.5em) translateZ(.5em);
		}
	}
}
<div class="perspective">
	<div class="bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
		<div class="bar-face roof"></div>
		<div class="bar-face front"></div>
		<div class="bar-face left"></div>
		<div class="bar-face right"></div>
		<div class="bar-face back"></div>
		<div class="bar-face floor"></div>
	</div>
</div>

pure-css-progress-1

Okay, this is a nice cube, but we want to build a rectangle for our bar. If you remember, we’ve already built the faces in a way to be liquid, so if we just increase the width of our .bar class we’ll get our shape. For this example we have used a width of 4em.

pure-css-progress-2

Building the Percentage Fills

The percentage fills will be contained inside our faces and, to keep our HTML code minimal, we’ll make use of the pseudo class :before. This generated :before element will grow to show the percentage relative to its face’s width.

.bar {
	// -> The SCSS from before
	.bar-face {
		// -> The SCSS from before
		&.percentage:before {
			content: '';
			display: block;
			position: absolute;
			bottom: 0;
			width: 0;
			height: 100%;
			margin: 0;
			background-color: rgba($magenta, .8);
			transition: width .6s ease-in-out;
		}
	}
}
<div class="perspective">
	<div class="bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
		<div class="bar-face roof percentage"></div>
		<div class="bar-face front percentage"></div>
		<div class="bar-face left"></div>
		<div class="bar-face right"></div>
		<div class="bar-face back percentage"></div>
		<div class="bar-face floor percentage"></div>
	</div>
</div>

pure-css-progress-2

Now we need to write the percentage fill style. It would be a tedious task to write one hundred classes by hand, so let’s write some sassy loop to get all the values for the aria-valuenow attribute from our HTML.

.bar {
	// -> The SCSS from before
	.bar-face {
		// -> The SCSS from before
	}

	@for $i from 0 to 101 {
		&[aria-valuenow='#{$i}'] {
			.percentage:before {
                width: $i * 1%;
			}
		}
	}
}

If you want to see it in action, just change the aria-valuenow attribute in the HTML from 0 to 100.

Tiny break: 📬 Want to stay up to date with frontend and trends in web design? Subscribe and get our Collective newsletter twice a tweek.

Building the Skin

For building our skins, we’ll use Sass mixins. In order to get a realistic look we’ll play with the box-shadow property. This property supports an array of values, and this array will let us emulate lightings. We’ll include the floor’s shadow and the face’s lighting in this property.

@mixin build-skin($color, $name) {
	&.#{$name} {
		.floor {
			box-shadow:
				0 -0.2em 1em rgba(0,0,0,.15),
				0 0.2em 0.1em -5px rgba(0,0,0,.3),
				0 -0.75em 1.75em rgba($white,.6);
		}
		.left {
			background-color: rgba($color, .5);
		}
		.percentage:before {
			background-color: rgba($color, .5);
			box-shadow: 0 1.6em 3em rgba($color,.25);
		}

	}
}
.bar {
	// -> The SCSS from before
	@include build-skin(#57caf4, 'cyan');
}
<div class="perspective">
	<div class="bar cyan" role="progressbar" aria-valuenow="75" aria-valuemin="0" aria-valuemax="100">
		<div class="bar-face roof percentage"></div>
		<div class="bar-face front percentage"></div>
		<div class="bar-face left"></div>
		<div class="bar-face right"></div>
		<div class="bar-face back percentage"></div>
		<div class="bar-face floor percentage"></div>
	</div>
</div>

pure-css-progress-2

Also, we need a trick to illuminate our faces. If we type our face’s DOM nodes in the correct order we’ll see the magic happening:

<div class="perspective">
	<div class="bar cyan" role="progressbar" aria-valuenow="75" aria-valuemin="0" aria-valuemax="100">
		<div class="bar-face roof percentage"></div>
		<div class="bar-face back percentage"></div>
		<div class="bar-face floor percentage"></div>
		<div class="bar-face left"></div>
		<div class="bar-face right"></div>
		<div class="bar-face front percentage"></div>
	</div>
</div>

pure-css-progress-2

What has happened here? It’s simple: while the browser is rendering an absolute element it adds an auto-incremented z-index by default (if we don’t edit this property). So, if we change the rendering order by putting the floor’s face first, its shadow will be over all the faces of the back side. That’s how we can apply a realistic shading.

And that’s all there is to this practical little progress bar! Now, make sure to check out all the demos and the files, and start building your own skins to train your skills.

Thank you for reading, we hope you enjoyed this tutorial and find it useful!

Rafael González

I craft web things, loving HTML and CSS. I type to the beat of music.

Stay in the loop: Get your dose of frontend twice a week

👾 Hey! Looking for the latest in frontend? Twice a week, we'll deliver the freshest frontend news, website inspo, cool code demos, videos and UI animations right to your inbox.

Zero fluff, all quality, to make your Mondays and Thursdays more creative!

Feedback 18

Comments are closed.
  1. Excellent article. Small suggestion, please include Edge in Browser Support. Edge supports this.

  2. Great article, thank you for sharing!
    It looks like the link to ‘Introduction to WAI ARIA’ goes to compass website, though.

    • Hi Callum!
      The exercise isn’t prefixed. I use an auto-prefixer to do this, if you’re (like me) using Sublime Text to develop code, you can install the auto-prefixer plug-in through the “Package Control”.

  3. You should not be bloating your markup with unnecessary divs; there is a progress element, and you should use it.

  4. An interesting use of CSS here to revitalise the tedious process that often involves waiting for something to load. Thanks, Rafael!

  5. I too agree with Vinoth’s opinion to include edge in browser support. Thanks Rafael for sharing this to show the interesting uses of CSS and codes.