RAY MORTIMER

UK Web Developer

CSS Only Interactions - No Javascript Required

When interacting with a web page and providing feedback based on user input, you might think that you need to use Javascript, specifically events, to handle that. While that's true, especially for complex interactions, you can actually achieve some level of interaction with CSS only!

A Little Trickery

We are going to use something often called the "radio button trick". When you click and select a radio button, its state changes to 'checked' and there is a pseudo class for this called :checked

So it is possible to modify the CSS properties of an element or more importantly its siblings, depending on whether it is checked or unchecked.

We also need to hide the radio button because its styling is likely not what we want. But then how can the user check it and make cool things happen in just CSS? For that we need to take advantage of labels.

Labels To The Rescue

<label> is an accompanying element for form fields that is predominantly used as a title or hint for that field.

But it has an interesting attribute: for. The value of the for attribute is set to the id of the form field it is related to (it needs to be id, not a class). Now here is the clever and useful bit - when the label is clicked, it will also check or uncheck its related radio form field (or checkbox, or move the cursor to a text field).

Example - Opening A Menu

Let's take a look at an example of using this - opening and closing a menu (full demo at the bottom of this page). Normally we would require Javascript to monitor a click event on the menu open/close buttons, and add a class to the menu to make it appear or disappear.

Without using Javascript, here is an example of the HTML we need:

<input id="menu-open" type="radio" name="menugroup" />
<input id="menu-close" type="radio" name="menugroup" />
<label class="label-open" for="menu-open">MENU</label>
<label class="label-close" for="menu-close">MENU</label>

We have two radio buttons in the menucontrol group (remember that within a group only one can be selected at one time). They have a suitable class name set to the action we want them to perform.

You will notice that the label elements are tied to their respective radio buttons with the for attribute.

Here is the CSS for this:

input[name="menugroup"] { display: none; }
.label-close { position: absolute; top: 0; left: 0; display: none; }
#menu-open:checked ~ .menu { max-height: 200px; }
#menu-open:checked ~ .label-close { display: block; }
#menu-close:checked ~ .label-close { display: none; }

What we are doing here is firstly hiding those ugly radio buttons, stacking the close label above the open label and setting the CSS for those user actions.

Checking Out :Checked

The real magic happens with the :checked declarations.

We are setting the menu height to be a maximum of 100px (from default is 0px so hidden from view) when the open label is clicked. We achieve this by using the ~ or sibling selector. This allows us to modify a different element to the label we clicked.

We can target siblings and descendants, but not elements that are outside our parent.

This does mean that the radio elements and the ones you want to modify are close together so we need to carefully plan our layout.

Why are we stacking a hidden close option on top of the open option and why is the text identical?

In Javascript we can take advantage of toggle but in CSS we have to write out each state.

We could provide separate open and close buttons but that would look a little odd. We could provide a close button as the last option on the menu which is better. But most people are used to clicking the same button to open and close a menu so by stacking them we can simply the interface.

When the open label is clicked, the CSS causes the menu to be revealed and also the menu close option to be made available.

Clicking this close option (now sitting over the open option) causes itself to be hidden again, and because it is now the active radio button and the open one is not, the CSS :checked declarations that made the menu appear are now removed. So the menu disappears.

Here is a demonstration, followed by the full code for this.

Live Demonstration

Full Code

<!doctype html>
<html lang="en-gb">
<head>
    <style>
        * { margin: 0px; padding: 0px; }
        html { font-family: sans-serif; }
        
        label { 
            cursor: pointer;
            font-size: 18px;
            margin: 0;
            height: 20px;
            width: 80px;
        }

        .menu-wrapper { position: relative; }
        input[name="menugroup"] { display: none; }     
        #menu-open:checked ~ .menu { max-height: 200px; }
        #menu-open:checked ~ .label-close { display: block; }
        #menu-close:checked ~ .label-close { display: none; }
        .label-close { 
            position: absolute; 
            top: 0; 
            left: 0; 
            display: none;
        }
        
        .menu { 
            list-style-type: none;
            padding: 0; 
            margin: 0; 
            max-height: 0; 
            transition: all 0.5s ease; 
            overflow: hidden;
        }
        .menu li { display: block; padding: 0 0 10px 10px; }
        .menu li:first-child { margin-top: 10px; }
       
        .label-close { 
            position: absolute; 
            top: 0; 
            left: 0; 
            display: none;
        }
    </style>
</head>


<body>
    <nav class="menu-wrapper">
        <input id="menu-open" type="radio" name="menugroup" />
        <input id="menu-close" type="radio" name="menugroup" />
        <label class="label-open" for="menu-open">MENU</label>
        <label class="label-close" for="menu-close"></label>
        
        <ul class="menu">
            <li><a href="#">Home</a></li>
            <li><a href="#">About</a></li>
            <li><a href="#">Blog</a></li>
            <li><a href="#">Contact</a></li>
        </ul>
    </nav>
</body>
</html>

Real World Uses

It is important to understand the implications of using something like this. It would work well for a a very simple website that would otherwise only need Javascript for an open/close menu like this demo, or as a backup option for important navigation should Javascript be disabled.

For more involved interactions, although it might technically be possible to use this method, it would probably require a lot of convoluted code that would compromise readability and would not give the slickness that Javascript could introduce.