Chewbacca

In the past, animated expansion and contraction of web page elements in response to user clicks has been tied to , particularly JQuery and other frameworks. I suspect this is partly due to web developer’s unfamiliarity with CSS3, the ubiquity of JQuery, desire for support in older versions of IE, and confusion about what selector to use: we know we can use :hover to initiate such events, but :hover is not a click. :focus and :active might work, but then how do you get “back” from that state to close the element again?

Before I proceed it should be noted that I’m hardly the first to explore this area: notably, Corey Mwamba wrote a particularly good article on the subject over at the excellent Opera developer site. What follows are merely my thoughts, with a little form element accessibility trickiness, and what I think is a simpler and more elegant solution.

It seems obvious that in order to toggle a window event we need an HTML element that has distinct “on” and “off” states. Two immediately spring to mind: radio buttons and checkboxes. In HTML5, they’re valid in almost any context. I’ll place a checkbox element to act as our toggle switch inside a div with a class of window. Directly underneath the checkbox, we’ll place the content that is going to be shown and hidden. Assuming that this could be a lot of content, I’ll place it inside its own <div>. So the markup will look like this:

<div class="window">
	<input type="checkbox" class="toggle">
		<div>
			<p>Here’s my content. Beep Boop Blurp.</p>
		</div>
</div>

Next, I’ll add some CSS. The outer <div> will be styled as our window container element:

div.window {
	color: white;
	width: 220px;
	padding: .42rem;
	border-radius: 5px;
	background: #c6a24b;
	margin: 1rem;
	text-align: center;
}

The inner <div> will initially be hidden by the fact that we’re going to set its height to 0 and overflow property to hidden, so that content does not “spill out” of the <div> and become visible:

div.window div {
	height: 0px;
	margin: .2rem;
	overflow: hidden;
}

That pulls our outer <div> closed. Now to initiate our expansion. First, we’re going to change the selector used above to be a little more precise:

input.toggle ~ div {
	height: 0px;
	margin: .2rem;
	overflow: hidden;
}

This is the sibling selector. It will provide the same result in your browser as the code above. If you’re saying to yourself “Why not use an adjacent selector instead?” you’re right... but we’ll see why a sibling selector is better choice in a moment.

Now we can get to our open / close routine. The CSS status for a checked input is, not surprisingly, :checked… so we’ll use that to set the height of the inner <div> to show all of its contents.

input.toggle:checked ~ div {
	height: 180px;
}

The neat part of this is the fact that we don’t have to worry about anything else: these two lines of code make the whole system work, and anything else we choose to add is an embellishment. Turning on the checkbox expands the inner <div>, which in turn expands the outer window.

“But I Don’t Want A Checkbox!”

Here’s the trick. We’re going to add a <label> after the checkbox. And we’re going to use the value of the label’s for attribute to link it to the checkbox, which will have a matching id. No CSS; this is all markup:

<div class="window">
	<input type="checkbox" class="toggle" id="punch">
	<label for="punch">Punch It, Chewie<label>
	<div>
		<p>Here’s my content. Beep Boop Blurp.</p>
	</div>
</div>

With the for attribute applied, the label acts as an alternate interface for the checkbox: clicking on the label is exactly the same as using the checkbox directly. Adding label elements has always been a best practice for form ; as is so often the case, knowing and using even a little accessibility opens up a world of opportunity for all users.

(It should also be clearer why we used the sibling selector: placing the label between the checkbox and the inner div in the markup renders an adjacent selector null and void).

You can probably anticipate where we’re headed next. We can completely turn off the checkbox, and leave just the label:

input.toggle { display: none; }

The result still works!

We can improve the default appearance of the label:

div.window label {
	display: block;
	background: #660b0b;
	border-radius: 5px;
	padding: .6rem;
}

We should also make it clear to the user that the label is an active interface:

div.window label:hover,
	div.window label:focus {
		cursor: pointer;
		background: #311;
}

It would also be nice to give the label a different look when the content is expanded:

input.toggle:checked + label {
	background: red;
}

Even though we no longer see the checkbox, its status can still influence the appearance of the label (and this is why the checkbox appears in the code before the label rather than after: we can’t yet do “reverse” selections in CSS).

Using a checkbox also makes it really easy to set the window to an open state by default: just place the checked attribute in the input:

<div class="window">
	<input type="checkbox" checked class="toggle" id="punch">

The label will continue to work: now clicking it the first time will close the window.

“Make It Go Sproing!”

So far there’s no animation: the window div simply snaps open and closed. We’ll smooth this motion and add a little bit of anticipation and follow-through.

What this means is that when the animation begins, the affected element will recoil slightly: think of a pitcher winding up in anticipation of throwing a speedball in a game of baseball, his body curving backwards before the pitch. At the end of the animation the element will overshoot its mark slightly before recovering to its final state.

input.toggle ~ div {
	height: 0px;
	margin: .2rem;
	overflow: hidden;
	transition: .6s all cubic-bezier(0.730, -0.485, 0.145, 1.620);
}

Conclusion

This technique has many possible applications, and just one downside: we must know the dimensions of the inner element when it is closed and open. You can define the element's height and width using whatever CSS unit you like, but we must state the dimensions explicitly. Alternatively, you can use max-height.