From our sponsor: Ready to show your plugin skills? Enter the Penpot Plugins Contest (Nov 15-Dec 15) to win cash prizes!
It all began with a small experiment that was inspired by a tutorial from Nettuts+ which shows a way how to embed a 3D bar chart into HTML pages using CSS, images and JavaScript. After reading the tutorial I challenged myself to turn this idea into pure CSS and see how far I can take it. The initial challenge was to create a classic semi-transparent 3D box with 6 sides. The final challenge was to create a complete 3D bar chart which we will create in this tutorial.
You can check out what I have done before here.
Let’s write down some key requirements. The chart should be
- background-independent
- adaptive (independent of the number of bars)
- scalable (just like vector graphics)
- easily customizable (colors, sizes and proportions)
The planning phase is the most important part of any project. So let’s make a plan.
Before actually coding, I usually write down all potential challenges with solutions I can think of in a specific project and repeat this process until I get something that looks like a strategy that can be executed. Here is the list of challenges with solutions I came up with for this project:
Challenge #1 – A bar with movable inner block
What we know:
- A bar should be represented as 3D box consisting of 6 sides
- Inner block should be vertically movable in motion. There should be an option to hide the block.
What we’ll need:
- 1 div for back casing consisting of 3 sides (back side, bottom side, left side)
- 1 div for front casing consisting of 3 sides (front side, top side, right side)
- 1 div for inner block consisting of 3 sides exactly as Front casing but with lower z-index
- 1 div container to position all three pieces relatively and apply a solid background patch in the bottom right corner
- 1 div container with overflow: hidden to hide the inner block under the bar when it goes down to zero
That makes a total of 5 divs.
You may wonder why would we need two containers? Well, it may be a tricky part but I will try to explain.
We need at least one container per bar (to hold front casing, back casing and inner block relatively to it). We know that our bar should be scalable, so we use percentages to manipulate the bar’s fill value, which requires our container’s height to be equal to the height of one of the bar’s sides.
Seems fine, but wait, looks like there is another problem – there should be an option to hide the inner block in motion, which means it should go “below the bar” and be hidden there. You may say we have a solution for that – overflow: hidden, right? Yes, but not for that container as its height is shorter than the actual height of the bar. That is why we add another container over it and apply overflow: hidden to it.
Hope this makes sense. Let’s move on.
Tiny break: 📬 Want to stay up to date with frontend and trends in web design? Subscribe and get our Collective newsletter twice a tweek.
Challenge #2 – The Graph Holder
The graph holder should
- be represented in 3D with axes and with 3 sides (background, bottom, left)
- be background-independent
- be adaptive to the number of bars and their attributes (height, width etc.)
- have X and Y axis labels from outside
What we’ll need:
- 1 unordered list
- 1 element inside each list item for the X axis labels
- 1 bar inside each list item
- 1 list item with an unordered list inside it for the Y axis labels
Hmm, unordered list? Isn’t it more semantic to use a definition list for a bar chart? Well, it is probably more semantic, but we can’t use it because we have to wrap every bar and its own X axis label in one container in order to position them relatively.
All right, but why wouldn’t we use a list item instead of the bar’s second container then? Well, we can’t do that because we have to place the X axis labels outside the graph and since we know that the bar’s second container hides any content that overflows it, we will use list items just to make sure all elements are positioned properly.
The Implementation
Now that we have a strategy, let’s convert it into code.
Please note that no vendor prefixes will be used in this tutorial. You can, of course, find them in the CSS files of the demo.
Challenge #1 – A bar with movable inner block
<div class="bar-wrapper"> <div class="bar-container"> <div class="bar-background"></div> <div class="bar-inner">50</div> <div class="bar-foreground"></div> </div> </div>
Let’s go over the purpose of each element once again:
- bar-wrapper – hides .bar-inner when it slides down below the bar
- bar-container – positions .bar-foreground, .bar-inner, .bar-foreground relatively and places patch for background in a bottom corner
- bar-background – creates 3 sides of casing: back, bottom, left
- bar-inner – the most important part probably – inner block
- bar-foreground – creates 3 sides of casing: front, top, right
First, let’s style the containers.
/* Bar wrapper - hides the inner bar when it goes below the bar, required */ .bar-wrapper { overflow: hidden; } /* Bar container - this guy is a real parent of a bar's parts - they all are positioned relative to him */ .bar-container { position: relative; margin-top: 2.5em; /* should be at least equal to the top offset of background casing */ /* because back casing is positioned higher than actual bar */ width: 12.5em; /* required, we have to define the width of a bar */ } /* right bottom patch - make sure inner bar's right bottom corner is "cut" when it slides down */ .bar-container:before { content: ""; position: absolute; z-index: 3; /* to be above .bar-inner */ bottom: 0; right: 0; /* Use bottom border to shape triangle */ width: 0; height: 0; border-style: solid; border-width: 0 0 2.5em 2.5em; border-color: transparent transparent rgba(183,183,183,1); }
Note that we set .bar-container‘s width to 12.5em. This number is a sum of the bar’s front and right sides widths – in our example it’s 10 + 2.5 = 12.5
We also use borders to shape the triangle and place it in the bottom right corner of .bar-container to make sure the inner bar’s side is “cut” when it moves vertically. We use the :before pseudo class to generate this element; we will use :before and :after pseudo classes a lot in this tutorial.
Now let’s style the back casing:
/* Back panel */ .bar-background { width: 10em; height: 100%; position: absolute; top: -2.5em; left: 2.5em; z-index: 1; /* just for reference */ } .bar-background:before, .bar-background:after { content: ""; position: absolute; } /* Bottom panel */ .bar-background:before { bottom: -2.5em; right: 1.25em; width: 10em; height: 2.5em; transform: skew(-45deg); } /* Left back panel */ .bar-background:after { top: 1.25em; right: 10em; width: 2.5em; height: 100%; /* skew only the Y-axis */ transform: skew(0deg, -45deg); }
As you can see we move the casing 2.5em up and right. And sure enough, we skew the left and bottom sides 45 degrees. Notice that we set the first skew value to 0deg, and the second one to -45deg which allows us to skew this element vertically.
It’s time to style the front casing.
/* Front panel */ .bar-foreground { z-index: 3; /* be above .bar-background and .bar-inner */ } .bar-foreground, .bar-inner { position: absolute; width: 10em; height: 100%; top: 0; left: 0; } .bar-foreground:before, .bar-foreground:after, .bar-inner:before, .bar-inner:after { content: ""; position: absolute; } /* Right front panel */ .bar-foreground:before, .bar-inner:before { top: -1.25em; right: -2.5em; width: 2.5em; height: 100%; background-color: rgba(160, 160, 160, .27); transform: skew(0deg, -45deg); } /* Top front panel */ .bar-foreground:after, .bar-inner:after { top: -2.5em; right: -1.25em; width: 100%; height: 2.5em; background-color: rgba(160, 160, 160, .2); transform: skew(-45deg); }
Nothing new here, everything is the same as in the back casing styles, we just use different directions.
The good part is that we applied those styles to both front casing and the inner block. Why not? They are exactly the same thing in terms of their shape.
All right, and now the styles for the inner block that we have not yet applied.
.bar-inner { z-index: 2; /* to be above .bar-background */ top: auto; /* reset position top */ background-color: rgba(5, 62, 123, .6); height: 0; bottom: -2.5em; color: transparent; /* hide text values */ transition: height 1s linear, bottom 1s linear; } /* Right panel */ .bar-inner:before { background-color: rgba(5, 62, 123, .6); } /* Top panel */ .bar-inner:after { background-color: rgba(47, 83, 122, .7); }
Great! The bars are all set. Let’s move on to the graph holder.
Challenge #2 – The Graph Holder (with axis labels)
<ul class="graph-container"> <li> <span>2011</span> <-- HTML markup of a bar goes here --> </li> <li> <span>2012</span> <-- HTML markup of a bar goes here --> </li> <li> <ul class="graph-marker-container"> <li><span>25%</span></li> <li><span>50%</span></li> <li><span>75%</span></li> <li><span>100%</span></li> </ul> </li> </ul>
As you can see we use an unordered list and span elements inside the items to position the X- and Y- axis labels.
/** Graph Holder container **/ .graph-container { position: relative; /* required Y axis stuff, Graph Holder's left and bottom sides to be positions properly */ display: inline-block; /* display: table may also work.. */ padding: 0; /* let the bars position themselves */ list-style: none; /* we don't want to see any default <ul> markers */ /* Graph Holder's Background */ background-image: linear-gradient(left , rgba(255, 255, 255, .3) 100%, transparent 100%); background-repeat: no-repeat; background-position: 0 -2.5em; }
The tricky part here is the background. We use a linear gradient to fill the graph container and lift it up by 2.5em. Why? Because our graph holder’s bottom side (which we will style next) is 2.5em high and is skewed by 45 degrees so there is an empty space in the right bottom corner.
Let’s style the bottom side now.
/* Graph Holder bottom side */ .graph-container:before { position: absolute; content: ""; bottom: 0; left: -1.25em; /* skew pushes it left, so we move it a bit in opposite direction */ width: 100%; /* make sure it is as wide as the whole graph */ height: 2.5em; background-color: rgba(183, 183, 183, 1); /* Make it look as if in perspective */ transform: skew(-45deg); }
We skew it by 45 degrees and move it a bit to the left just to make sure it is positioned properly.
Now let’s style left the side of our graph holder:
/* Graph Holder left side*/ .graph-container:after { position: absolute; content: ""; top: 1.25em; /* skew pushes it up so we move it down a bit */ left: 0em; width: 2.5em; background-color: rgba(28, 29, 30, .4); /* Make it look as if in perspective */ transform: skew(0deg, -45deg); }
Nothing special here. Just skewed the element by 45 degrees as usual and pushed it down a bit in order to position it properly.
We’re done with the graph holder. Now let’s add some magic to the list items that hold our bars:
/* Bars and X-axis labels holder */ .graph-container > li { float: left; /* Make sure bars are aligned one next to another*/ position: relative; /* Make sure X-axis labels are positioned relatively to this element */ } /* A small hack to make Graph Holder's background side be wide enough ...because our bottom side is skewed and pushed to the right, we have to compensate it in the graph holder's background */ .graph-container > li:nth-last-child(2) { margin-right: 2.5em; } /* X-axis labels */ .graph-container > li > span { position: absolute; left: 0; bottom: -2em; width: 80%; /* play with this one if you change perspective depth */ text-align: center; font-size: 1.5em; color: rgba(200, 200, 200, .4); }
A few things happened here. First of all, we float our bars next to each other. Usually, I’m trying to be very careful with using floats, but in this case it fits perfectly in my opinion.
Secondly, we add some right margin to the last bar. That way we make sure we give enough space to the graph holder’s bottom side to show up in the right bottom corner. Try to remove it and you will see what I mean.
OK, we’re almost there. The last thing left is to add Y-axis markers..
/* Markers container */ .graph-container > li:last-child { width: 100%; position: absolute; left: 0; bottom: 0; } /* Y-axis Markers list */ .graph-marker-container > li { position: absolute; left: -2.5em; bottom: 0; width: 100%; margin-bottom: 2.5em; list-style: none; } /* Y-axis lines general styles */ .graph-marker-container > li:before, .graph-marker-container > li:after { content: ""; position: absolute; border-style: none none dotted; border-color: rgba(100, 100, 100, .15); border-width: 0 0 .15em; background: rgba(133, 133, 133, .15); } /* Y-axis Side line */ .graph-marker-container > li:before { width: 3.55em; height: 0; bottom: -1.22em; left: -.55em; z-index: 2; /* be above .graph-container:after */ transform: rotate(-45deg); } /* Y-axis Background line */ .graph-marker-container li:after { width: 100%; bottom: 0; left: 2.5em; } /* Y-axis text Label */ .graph-marker-container span { color: rgba(200, 200, 200, .4); position: absolute; top: 1em; left: -3.5em; /* just to push it away from the graph.. */ width: 3.5em; /* give it absolute value of left offset */ font-size: 1.5em; }
As you can see, we set 100% width to our markers holder in order to be able to draw throughout the whole graph, use a dotted border to style our Y-axis lines and position the span element so that the Y-axis label is outside of the graph. With the help of :before and :after we could keep our HTML pretty clean.
Well, we finished setting up all the styles for our graph, however we didn’t set some vital variables – sizes, colors and bars fill values! We said that our graph will be customizable, right? So, I decided not to mix variables with the rest of the code so that you can play with them..
/**************** * SIZES * ****************/ /* Size of the Graph */ .graph-container, .bar-container { font-size: 8px; } /* Height of Bars */ .bar-container, .graph-container:after, .graph-container > li:last-child { height: 40em; } /**************** * SPACING * ****************/ /* spacing between bars */ .graph-container > li .bar-container { margin-right: 1.5em; } /* spacing before first bar */ .graph-container > li:first-child { margin-left: 1.5em; } /* spacing after last bar */ .graph-container > li:nth-last-child(2) .bar-container { margin-right: 1.5em; } /**************** * Colors * ****************/ /* Bar's Back side */ .bar-background { background-color: rgba(160, 160, 160, .1); } /* Bar's Bottom side */ .bar-background:before { background-color: rgba(160, 160, 160, .2); } /* Bar's Left Back side */ .bar-background:after { background-color: rgba(160, 160, 160, .05); } /* Bar's Front side */ .bar-foreground { background-color: rgba(160, 160, 160, .1); } /* Bar's inner block */ .bar-inner, .bar-inner:before { background-color: rgba(5, 62, 123, .6); } .bar-inner:after { background-color: rgba(47, 83, 122, .7); } /************************************* * Bars Fill * * Just an example of filling 3 bars * *************************************/ .graph-container > li:nth-child(1) .bar-inner { height: 25%; bottom: 0; } .graph-container > li:nth-child(2) .bar-inner { height: 50%; bottom: 0; } .graph-container > li:nth-child(3) .bar-inner { height: 75%; bottom: 0; }
In the demo provided with this tutorial you will not find this part as it is here, because I did something even more interesting there – I used radio buttons to let you play with the variables without modifying the code. Feel free to check out its source code. But if you just need to customize a static graph grab the code snippet from above and customize it to your preference.
Conclusion
Let’s go over some featured CSS specifications/techniques we covered in this tutorial. So, we’ve used
- transform: skew() and transform: rotate() in order to transform our elements so that together they generate an illusion of a 3D object
- :before and :after pseudo classes to generate elements with CSS and keep our HTML markup relatively clean
- :nth-last-child() and :not pseudo classes to target specific list items and avoid adding extra classes/ids to the markup
- linear-gradient together with background-position to partially fill an element with a background
- rgba() for colors with alpha-transparency
- borders to create shapes like a triangle
I really hope that this tutorial was useful to you. If you have any questions regarding this tutorial, let me know in the comments below.
Outstanding! Very inspiring.
Hi Sergey Lukin… Your work is awesome… I liked it very much… I will use in my future projects… Thanks for sharing…
Pretty Awesome. Love it… 😉
Awesome, awesome, awesome!
Gorgeous! Looks fantastic.
Always a pleasure to see Sergey’s innovations. A true master. Keep it up!
great one, love it 🙂
Absolutely stunning! Awesome work!
This is exactly what I was looking for. Thank you!
It’s pity that so bad examples are spreading via web-developers. This example is based on drafts of CSS specifications, which can change in any time. And it already have some mistakes.
So, “CSS Transforms” specification doesn’t have a level (despite “css3” in url). But the worse part is that there is no skew(x, y) function. And it’s bound to not to be there.
Furthermore, pseudo-elements
::before
and::after
are written with a single colon while they should have two as specified in “Selectors Level 3”. While specification allows single-colon notation for good old pseudo-elements, it should be considered as a bad practice, since rules such astransform
are being used in this example, which old CSS 2.1 browsers like IE8 will not understand.No explanation is given about specifications, and what things are belong to which specifications. Therefore, I believe, such rude mistakes are present here.
Looks fantastic. Thank you!
Awesome, awesome, awesome, awesome, awesome, awesome, awesome, awesome, awesome, awesome,
one word :
damn!
very clear, and very creatif, bravo, that one was awsome
This is a piece of art!
Looks great. The only (bad) thing catching my eye is that all the boxes have a shadow on their right face except the blue. For some reason the blue box has a lighter right side, and all the others have a darker right side.
Great job! but i wonder who could integrate this on his website efficiently.. 🙁 it seems hard
So good \m/
@sabow I’m currently working on Code Generator, stay tuned to repo on Github.
Thanks to everyone for your feedback. Really appreciate
Very cool,