5 things you won’t believe are only built with CSS

mm by Trevan Hetzel on July 30, 2014

The modern web is always changing, and this article is more than two years old.

Trevan Hetzel breaks down 5 UI interactions typically built with JavaScript, implemented only with CSS.

By Trevan Hetzel

I often find myself amazed at what plain ‘ol CSS can do. People float things with CSS, they clear things, they color things, they make things fancy. But there are a lot more hidden, or I should say “less common”, things that CSS can accomplish with a little bit of trickery and creativity that many people shove to the side in favor of quicker and easier JavaScript solutions. I’ve been on a journey for the past year or so, forcing myself to create components with pure CSS. The following techniques are essentially solutions to mimic JavaScript click events, but for this article we’re pure CSS!

#1 – Styling the all-powerful checkbox

Throughout the methods described in this article, there’s one HTML element that you’ll see used quite a bit: the checkbox. Tons of resources pop up when you Google “CSS checkbox hack”, and it’s for good reason. The HTML checkbox and radio elements have :checked pseudo-classes that are very powerful. The premise is that you can hide the actual checkbox element and put your styling on its associated label. Clicking the label will activate and deactivate the :checked pseudo-class of the checkbox. You can then use adjacent and general sibling selectors to control the appearance of elements that come after (either directly or down the road as siblings) the checkbox, the same way you would use JavaScript to toggle classes on elements. Of course, you can’t modify elements before the checkbox, but there’s still a lot you can do with this technique.

An example in the simplest form is styling standard form checkboxes. There are some discrepancies on how different browsers render the checkbox element and there’s not as much freedom styling checkboxes as other HTML elements, so the easiest way to provide consistent styling is to use the technique described above and put the styling on the checkbox’s label. The markup needed contains nothing special. Just a label and a checkbox, but be aware that the label needs to come after the checkbox, as you’ll see in a bit.

<input id="sample-checkbox" class="checkbox" type="checkbox">
<label class="label" for="sample-checkbox">Click me</label>

The first thing to do with CSS is to hide the checkbox. Then we’ll add a pseudo-element to the label. This :before (or :after, either works) pseudo-element is the “simulator”, the thing that actually looks like a checkbox.

.checkbox {
    display: none;
}

.label:before {
    content: "";
    margin: 0 .5em 0 0;
    float: left;
    width: 1em;
    height: 1em;
    background: #fff;
    border: 1px solid #222;
    border-radius: 3px;
}

basic-checkbox

Simple as that, we have a custom checkbox! The only thing missing now is the alteration when it’s checked. To accomplish this part, we can utilize the :checked pseudo-class and CSS adjacent sibling selector to alter the appearance of the label since it comes after the checkbox in the DOM. This is why, as I mentioned above, the label needs to come after the checkbox.

.checkbox:checked + .label:before {
    content: "✓";
}

basic-checkbox-checked

#2 – Using CSS to show or hide content

With an understanding of the general concept behind this checkbox “hack”, we can do some much cooler stuff than just styling form elements. A good example of this is showing and hiding content, much like a JavaScript toggling effect. To use an example, let’s pretend we have a list that has a “more” link below the first five items. Clicking the “more” link should show the rest of the items in the list.

We can keep the markup super simple and just use styled anchor tags for the list items. The whole list would look like this:

<a href="#">Micket</a>
<a href="#">Mace Windu</a>
<a href="#">Count Dooku</a>
<a href="#">Admiral Ackbar</a>
<a href="#">Padme Amidala</a>
<a href="#">Gamorrean Guards</a>
<a href="#">C-3P0</a>
<a href="#">Qui-Gin Jinn</a>
<a href="#">Imperial Guards</a>
<a href="#">Obi-Wan Kenobi</a>
<a href="#">Tie Fighter Pilot</a>
<a href="#">Greedo</a>

To hide all but the first five, we’ll just wrap the last seven in a div.

<a href="#">Micket</a>
<a href="#">Mace Windu</a>
<a href="#">Count Dooku</a>
<a href="#">Admiral Ackbar</a>
<a href="#">Padme Amidala</a>

<div class="all-items">
    <a href="#">Gamorrean Guards</a>
    <a href="#">C-3P0</a>
    <a href="#">Qui-Gin Jinn</a>
    <a href="#">Imperial Guards</a>
    <a href="#">Obi-Wan Kenobi</a>
    <a href="#">Tie Fighter Pilot</a>
    <a href="#">Greedo</a>
</div>

First, let’s make the list a little prettier than the standard browser default. We’ll just target all a tags (use a class for production!).

a {
    display: block;
    border-bottom: 1px solid #ccc;
    padding: 1em 2em;
    width: 100%;
    color: #000;
    text-decoration: none;
}

Then we’ll hide the last seven items that are wrapped in the .all-items div by setting display: none on .all-items.

Now for the fun part. Directly before the .all-items div is where we’ll add a checkbox and label. This allows us to target the succeeding div with an adjacent sibling selector.

<input id="show-all" class="show-all" type="checkbox">
<label for="show-all"></label>

Since we’re not really using the visual portion of the checkbox, let’s hide by simply setting display: none.

.show-all {
    display: none;
}

At this point, since there’s nothing inside the label, you won’t see anything different than the original list. The reason there’s nothing inside the label is because we need it to say two things: “More” (when there’s more items to display) and “Less” (when there’s fewer items to display). To control that, you can put the actual content in a pseudo-element, like so:

label::before {
    content: "More";
}

To make it match the styling of the rest of the list, let’s add a bit of spunk by just applying the styles of anchors to it. We’ll also add a cursor: pointer rule to make the label feel like an actual link.

a,
label {
    display: block;
    border-bottom: 1px solid #eaedf1;
    padding: 1em 2em;
    width: 100%;
    color: #9ea7b3;
    font-size: 1.125em;
    text-decoration: none;
    cursor: pointer;
}

And now for the actual toggling. This is actually the simplest part:

.show-all:checked ~ .all-items {
    display: block;
}

Even though the checkbox is hidden, it’s corresponding label still acts as its right-hand man. Clicking him toggles on and off the :checked psuedo-class of the actual checkbox. With that, we’re just targeting with the general sibling selector that hidden div that contains the rest of the items we want to show. Pretty easy, eh?

The reason that we used the ~ (general sibling selector) instead of the adjacent sibling selector (+) is because, like I touched on before, we want to be able to control the words of the label. Since the label comes directly after the checkbox in the DOM, using an adjacent sibling selector would target the label, not the div that we’re intending to target.

Now that we’ve got the toggling working, there’s a couple things that still aren’t quite right. The first is that the “trigger” (the “more” link) is still in the middle of the list, instead of at the bottom, below the newly shown items. To get it to the bottom, we’re going to have to modify a little positioning. The only way to move things around in the DOM, besides using JavaScript to actually move them, is to alter the positioning. We’ll start out by wrapping an element around the list and set its position property to relative.

The final markup for this example looks like this this:

<div class="list">
    <a href="#">Micket</a>
    <a href="#">Mace Windu</a>
    <a href="#">Count Dooku</a>
    <a href="#">Admiral Ackbar</a>
    <a href="#">Padme Amidala</a><input id="show-all" class="show-all" type="checkbox" />

    <label for="show-all"></label>

    <div class="all-items">
        <a href="#">Gamorrean Guards</a>
        <a href="#">C-3P0</a>
        <a href="#">Qui-Gin Jinn</a>
        <a href="#">Imperial Guards</a>
        <a href="#">Obi-Wan Kenobi</a>
        <a href="#">Tie Fighter Pilot</a>
        <a href="#">Greedo</a>
    </div>
</div>

Then we’ll position the label to the bottom of that list, so it’s always at the bottom.

label {
    position: absolute;
    bottom: 0;
}

We’re getting somewhere, but it still looks pretty wonky.

list-wonky

To get around the text overlapping the last item, a padding-bottom is in store for the containing .list div. We’ll make an educated guess as to that padding-bottom value (by looking at the calculated height from the Dev Tools of the label) and set it to to 3.188em.

Now we’re getting somewhere!

list-more

The last thing is to change the text from “More” to “Less” while the list is expanded. Using the adjacent sibling selector, the content value of the label is all that needs changed.

.show-all:checked + label:before {
    content: "Less";
}

Final CSS only toggle

Check out this Pen!

#3 – Off-canvas navigation

Another example of some functionality that is most commonly done with JavaScript is off-canvas navigation. Off-canvas is pretty common on responsive sites for hiding content “off” the screen at small breakpoints. Typically, with JavaScript, you would just add a class to a containing element and move things around with CSS, or even use JavaScript (probably not as performant) to slide the off-canvas in. We can take a very similar approach for this example, but never use JavaScript. It’s done, again, using a checkbox.

Let’s look at the markup needed to pull this off.

<input type="checkbox" id="checkbox" class="checkbox">
<label for="checkbox" class="icon"></label>
<label for="checkbox" class="body-close"></label>
<aside class="off-canvas">...</aside>
<section class="wrapper">...</section>

As before, we’ll hide the checkbox. The first label, .icon will serve as the “trigger” and will be styled like the common hamburger icon. The second label is a little less obvious as to what it does, but it is used to close the off-canvas when it’s open by clicking anywhere on the page other than inside the actual off-canvas. Then of course .off-canvas will house the actual off-canvas navigation, and .wrapper contains everything else on the page that will be slid over when the off-canvas is open.

Setting up the base styles

.checkbox {
    display: none;
}

.icon {
    position: absolute;
    right: 0;
    width: 1.625em;
    height: .313em;
    background: #000;
    margin: 1.5em;
    transition: right 0.3s ease;

    &::before,
    &::after {
        content: "";
        position: absolute;
        background: #000;
        width: 1.625em;
        height: .313em;
    }

    &::before {
        top: -.625em;
    }

    &::after {
        bottom: -.625em;
    }
}

.body-close {
    position: absolute;
    top: 0;
    right: 17.5em;
    width: 100%;
    height: 100%;
    pointer-events: none;
    z-index: 1;
}

.off-canvas {
    position: fixed;
    right: -17.5em;
    width: 17.5em;
    height: 100%;
    background: #ccc;
    transition: right 0.3s ease;
}

.wrapper {
    position: relative;
    left: 0;
    transition: left .3s ease;
}

Giving it functionality

With the positioning and styling in place, implementing the :checked pseudo-class on the checkbox is all that’s left to make this actually work. Four things have to change when it’s checked:

  • the right value of .off-canvas (this is where it “slides” in, using a CSS transition)
  • the right value of .icon (so it slides over as well, appearing like it’s being pushed to the side by the off-canvas)
  • the pointer-events value of .body-close (we set it none originally, as it basically covers the entire page and you need to be able to click through it; when the checkbox is checked, however, we reset this value to auto so clicking it closes the off-canvas)
  • the left value .wrapper (to push over the main content)
.checkbox:checked ~ .off-canvas {
    right: 0;
}

.checkbox:checked ~ .icon {
    right: 17.5em;
}

.checkbox:checked ~ .body-close {
    pointer-events: auto;
}

.checkbox:checked ~ .wrapper {
    left: -17.5em;
}

Final CSS only off-canvas

Check out this Pen!

#4 – Meet the :target pseudo selector

I’ve covered a few examples of some somewhat tricky techniques by way of HTML checkboxes, but there’s another way to recreate a lot of JavaScript-ish functionality with pure CSS: the :target psuedo-selector. The :target pseudo-class is activated on an element with the URL hash is the same as an element’s ID. For instance, if the URL is http://localhost:3000/#whatsup, then we can manipulate the styles of the element with an ID #whatsup with #whatsup:target.

Keep in mind, :target is a CSS3 property, so it’s not supported in IE8.

A simple use case is showing content, similar to what I walked through above with the checkbox technique, but not exactly a “toggle”.

<a href="#toggle">Show content</a>
<p id="toggle">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse in lectus nec orci vestibulum suscipit quis sodales tortor. Maecenas vitae hendrerit orci, eu faucibus libero.</p>
#toggle {
    display: none;
}

#toggle:target {
    display: block;
}

As you can see, clicking the anchor will change the hash in the URL to /#toggle, which activates the :target pseudo-class on the #toggle element. Thus, we can display the paragraph when a “trigger” is clicked. However, there are a few things that this does that might not be ideal for every situation:

  • It manipulates browser history (hitting the back button would not take you back a page, but just back a step before the hash was added)
  • It jumps the page to wherever the #toggle element lives.
  • There’s no way to get back to the original state. Clicking the anchor again, unlike checkboxes, will not “unhash” the URL.

We can’t really control the first two of those things without some JavaScript, but we can get creative and provide a workaround for the third thing (getting back to the original state). The easiest would be to put another anchor after the paragraph that’s only shown when the paragraph is shown. The href of this new anchor could be anything (a simple # would do), as long as it gets the hash away from the ID of the paragraph.

<a href="#toggle">Show</a>
<p id="toggle">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse in lectus nec orci vestibulum suscipit quis sodales tortor. Maecenas vitae hendrerit orci, eu faucibus libero.</p>

<a href="#" class="close">Close</span>
.close {
    display: none;
}

#toggle:target + .close {
    display: block;
}

A more involved way to make a true toggle with :target would probably require some positioning modifications to position the anchors absolutely (so they appear before the paragraph), but actually put them after the paragraph in the DOM order so you can utilize general sibling selectors to target their appearance.

Final :target show/hide

Check out this Pen!

Off-canvas, revisited

With this :target technique, let’s revisit the off-canvas but this time build it with the :target pseudo-selector instead of a checkbox. The end result will be essentially the same thing from a user’s perspective.

We can re-use some of the HTML elements, but will need to replace the labels with anchors, and move the icon anchor below the .off-canvas element.

<aside id="off-canvas" class="off-canvas">...</aside>
<a href="#off-canvas" class="icon"></a>
<a href="#" class="body-close"></a>
<section class="wrapper">...</section>

The CSS is also pretty much the same, except instead of using .checkbox:checked we need to use .off-canvas:target. For brevity, I won’t list out the whole CSS again (since it’s the same as above), but instead just the :target pseudo-selector styles. You can view the full source in the CodePen below.

.off-canvas:target {
    right: 0;
}

.off-canvas:target ~ .icon {
    right: 17.5em;
}

.off-canvas:target ~ .body-close {
    pointer-events: auto;
}

.off-canvas:target ~ .wrapper {
    left: -17.5em;
}

As with the checkbox method, there’s a .body-close element that covers the whole page to act as a trigger to close the off-canvas when it’s open, but this time instead of it triggering a checkbox state change it triggers a new hash in the URL (just #) to deactivate the :trigger pseudo-class.

Final :target off-canvas

Check out this Pen!

#5 – Building a Modal Dialog with only CSS

One last case where you can use CSS to recreate JavaScript-like click events is that of a common pop-up modal. Using :target, you can actually make really nice modals that have close buttons and even close when you click “off” the modal.

The markup just needs an anchor with the href set to the ID of the containing modal element.

<a href="#samplemodal">Show modal</a>

<div id="samplemodal" class="modal-contain">
    <a href="#" class="modal-close"></a>
    <div class="modal">
        <a href="#">X</a>
        <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus hendrerit augue eu tincidunt sollicitudin. Morbi bibendum enim vel mauris ultrices volutpat. Nam imperdiet ultrices faucibus. Vivamus mattis tempus ipsum, sit amet faucibus tortor porta at. Sed tincidunt mauris in risus euismod adipiscing. Nullam ullamcorper arcu sit amet odio sodales, et sollicitudin ipsum eleifend. Proin mattis nunc aliquet diam varius porta. Donec pellentesque non metus ut bibendum.</p>
    </div>
</div>

Notice the empty .modal-close anchor. That’s there to handle closing the modal when you click “off” of it (anywhere on the page other than inside the modal), and will just be positioned absolutely behind the .modal element. In terms of what we’ve covered in the article so far, the CSS is pretty straightforward. We’re just displaying .modal-contain when its ID matches the hash that’s added to the URL when you click the trigger element.

.modal-contain {
    display: none;
}

.modal-contain:target {
    display: block;
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0,0,0,.75);
}

.modal-close {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    cursor: default;
}

.modal {
    position: absolute;
    top: 50%;
    left: 0;
    right: 0;
    transform: translateY(-50%);
    margin: 0 auto;
    width: 25em;
    padding: 1em;
    background: #fff;
}

Final :target modal

Check out this Pen!

In closing

I’ve covered several use cases for using CSS to create click events, but there are many other things you can do with these techniques. Using checkboxes and :target opens up the door quite a bit to things you can pull off with pure CSS. I’m finding myself amazed at the amount of times I can realistically rely on these techniques, but there of course times where you’ll need to bring in some JavaScript to pick up the slack of CSS (until we can traverse up the DOM tree with CSS!). Happy experimenting!

10 comments"

  1. Yuming Cheung says:

    #1 seems not working in the codepen.
    Do I miss anything?
    http://codepen.io/ymcheung/pen/sopqH

    1. It looks like the quotes in the `content` property are not correct. They must have copied over as the wrong entities. Changing line 17 to `content: “✓”;` gets it working.

  2. Josh says:

    I know that your post is for demonstration purpose only, but what I often times miss when reading content about “CSS only”, is the fact, that it’s most of the times poorly accessible. Widgets should always be accessible using keyboard, and for this, JS is needed in 90% of the time.

  3. mattecapu says:

    In the third example, it is sufficient to move the label after the hidden elements and replace the adjacent selector used for label text change with a sibling selector

    http://codepen.io/anon/pen/FqoBh

    1. mattecapu says:

      *second example, sorry

  4. George Mauer says:

    Ok, that css modal thing is pretty damn exciting. Kind of a shame that it would clash with half the routing frameworks out there, but screw them.

    Question since the first example isn’t on codepen I can’t experiment with it. But can you position the checkbox in the label for that? It seems like it would work and I really hate associating labels by id.

  5. Nicolas says:

    I tested the off-canvas trick, but doesn’t seem to work on mobile android web browsers… Any idea ?

Leave a Reply

Your email address will not be published. Required fields are marked *

© 2016 Modern Web & our authors. All rights reserved.