CSS Click Events

An article about different experimental approaches of employing click events using CSS only. It summarizes and shows some clever hacks and smart tricks.

CSS Click Events

For the last few years, we’ve been witnessing the wonderful expansion of front-end languages especially HTML with the HTML5 specifications and CSS with the CSS Level 3 specifications.

We can now do a lot of stuff we couldn’t have done without JavaScript or images before, like rounded corners, gradients, responsive layouts, grid stuff, transparency in colors, and so much more.

But one thing we’ve always been missing is the possibility to handle click events with CSS. Actually, some people think we shouldn’t have this option since interactions are more like a JavaScript kind of stuff. I understand the idea, but it always bothered me to have to use JavaScript for a very simple click event.

Anyway, as of today, CSS doesn’t provide any official way to handle a click event in CSS. But there are some very interesting tricks that we can use to “detect” a click using CSS only, without a single line of JavaScript, and this is what we are going to talk about today.

Disclaimer

This blog post is about showing the possibilities of CSS and some clever hacks. It’s clearly not for “real life” use cases. Please, consider the whole article as a playground for experimenting, not as a tutorial to handle click events on your website or application.

Plus, some of these techniques are not well supported by browsers, meaning it’s even more borderline; we intend to have some fun pushing the limits of CSS.

Checkbox hack

Aaaaah, the checkbox hack. What a thing my friends! I’m sure you’ve all heard about the checkbox hack before. It’s probably the most common way to handle a click event in CSS. It relies on, well, a checkbox.

What’s cool about a checkbox is the fact that it’s binary. It’s either checked or not checked. There is no “half-way” checked. This is why the checkbox hack is a pretty reliable way to trigger a click event in CSS. Let’s make an example.

How it works

The HTML

<input type="checkbox">
<p class="to-be-changed">I'm going to be red! It's gonna be legen... Wait for it...</p>

The CSS

.to-be-changed {
	color: black;
}

input[type=checkbox]:checked ~ .to-be-changed {
	color: red;
}

As you can see, it relies on the :checked pseudo-class and on the general sibling selector ~. Please note that it also works like a charm with the adjacent sibling selector +. Basically, it says “if the checkbox is checked, then the following elements with the .to-be-changed class will be red”.

Okay, a checkbox isn’t very sexy, but you can totally make something nicer by hiding the checkbox and binding a label to it. Something like this maybe:

<input type="checkbox" id="toggle">
<label for="toggle">Click me!</label>
<p class="to-be-changed">I'm going to be red! It's gonna be legen... Wait for it...</p>

So we hide the checkbox and use the label to trigger the click event.

input[type=checkbox] {
	position: absolute;
	top: -9999px;
	left: -9999px;
}

label {
	display: block;
	background: #08C;
	padding: 5px;
	border: 1px solid rgba(0,0,0,.1);
	border-radius: 2px;
	color: white;
	font-weight: bold;
}

input[type=checkbox]:checked ~ .to-be-changed {
	color: red;
}

This way, you have some kind of button triggering the color change on the paragraph. Isn’t that cool? Re-clicking on the button is switching the color back to black of course.

(Note that there are different possibilities for hiding the checkbox, the most obvious of all being display:none.)

You can see a demo here: Checkbox hack demo

Pros & Cons

Pros
  • Binary, checked or not checked
  • Clicking again gets you back to the first state (can also be a con if you consider having to click a second time is a pain)
  • Allows chained click events (if first checkbox clicked and second checkbox clicked then do something)
Cons
  • Elements have to share the same parent (siblings)
  • Requires some extra HTML (input, label, etc.)
  • Requires two workarounds to make it work on mobile browsers (please refer to further readings)

Further reading

There are like a bazillion demos with the checkbox hack since it’s clearly the most common way to handle a click event with pure CSS. However, there are probably a few things you may want to read about it:

The :target way

There is another way, well known to “fake” a click event with CSS, using the :target pseudo-class. This pseudo-class is quite similar to the :hover one in the way that it matches only a specific scenario.

The special event for the :target pseudo-class depends on what we call a “fragment identifier”. To put it simple, this pseudo-class refers to a hashtag you can see sometimes at the end of the URL. So it matches when the hashtag and the ID of an element are the same.

How it works

The HTML

<a href="#id">Click me!</a>
<p id="id" class="to-be-changed">I'm going to be red! It's gonna be legen... Wait for it...</p>

The CSS

.to-be-changed {
	color: black;
}

.to-be-changed:target {
	color: red;
}

Basically, when clicking on the link (href="#id"), the URL changes and you go to the anchor #id in the page. In this very moment, the element having this id can be targeted with the :target pseudo-class.

You can see a demo here: :target way demo

Pros & Cons

Pros
  • Very light CSS
  • Perfect for highlighting sections
Cons
  • Messes with the browser history
  • Makes a page jump (when reaching to the anchor)
  • Requires an anchor tag to trigger the hashtag or a URL hack
  • Can only affect one element (since IDs are unique)
  • No way of getting back to first state without another hashtag, a link, or a URL hack

The browser support isn’t great but honestly, it’s not that bad. All major browsers support it well except Internet Explorer 6 to 8. So starting from IE9, you’re good.

Further reading

The :focus way

Let’s continue with another way using a pseudo-class; the :focus one this time. It’s pretty much the same idea, except, it doesn’t expect a URL change. It relies on the user’s focus on a particular element.

When you’re on a web page, you can press the tab key to navigate through various elements on the page. It’s particularly useful when filling forms, to go from one field to another without having to use the mouse. It’s also used by blind or visually impaired people to navigate through a site.

What’s important to note is that some elements can be focused, like links, inputs and such, and some other can’t, like paragraphs, divisions, and plenty others. Actually they can, but you’ll need to add the tabindex attribute with a numeric value.

How it works

The HTML

<span tabindex="0">Click me!</span>
<p class="to-be-changed">I'm going to be red! It's gonna be legen... Wait for it...</p>

The CSS

span:focus ~ .to-be-changed {
	color: red;
}

So, when you click on the span or reach it with the tab key, it becomes focused and matches the :focus pseudo-class. The adjacent sibling selector does the rest. Pretty easy, right? If you don’t want to mess with the tabindex for any reason, you can simply use a link with a # href. It will work like a charm as well.

You can see a demo here: :focus way demo

Pros & Cons

Pros
  • Very light CSS and HTML
  • Great for navigation or similar
Cons
  • Requires either a focusable element or the tabindex attribute
  • Matches only when the element is focused (which means clicking elsewhere will mess it up)

Further reading

Transition hack

This is probably the most wicked way to handle a click event in CSS. Seriously guys, this is madness. This technique comes from Joel Besada and has always been one of my favorite CSS tricks.

The idea is to store a CSS style in a CSS transition. Yeah, you read it right, a CSS transition. Actually, the idea is pretty simple. It relies on applying a pseudo-infinite delay to a change in order to prevent it to get back to the default value. It may sound complicated but it’s fairly easy, trust me. Please have a look at the code.

How it works

The HTML

<span>Click me!</span>
<p class="to-be-changed">I'm going to be red! It's gonna be legen... Wait for it...</p>

The CSS

.to-be-changed {
	transition: all 0s 9999999s;
}

span:active ~ .to-be-changed {
	transition: all 0s;
	color: red;
}

The idea behind the first declaration is to delay any change to approximately 116 days to make sure the changes will stay once they’ve been set. It is not infinite, but kind of, right?

But we don’t want to apply the changes 116 days after clicking, we want it to be set immediately! So the idea is to override the delay during the click (:active pseudo-class) to apply the changes. Then when the click will be released, the old transition property will kick back in, setting back the delay to 9999999s, preventing the changes to going back to the default state.

You can see a demo here: Transition hack demo

Pros & Cons

Pros
  • Insanely clever
  • Keeps the exact value when releasing the click, even if the transition isn’t finished yet (see below)

Like Joel Besada states:

[…] a cool fact is that you can end a transition halfway through and have the property still keep the exact value that it had at that point in the transition. So for example, let’s say we have a linear transition from opacity 1 to 0 over two seconds, but only let it run for one second. The opacity value should now be stuck at 0.5 because the delay prevents it from going back. If we now were to run the opacity transition again, it would continue from 0.5 instead of starting over from the beginning.

Cons
  • Decent but not so good browser support (not IE9- and Opera Mini)
  • Only works with transitionable values
  • No way of getting back to the first state
  • Not really infinite if you can afford waiting 116 days

Further reading

You should definitely read the original article by Joel Besada:

Final words

As of today, we can do some click detections with CSS. I mean, this is possible even if it’s pretty much a hack in any case. Some ways are better than others for some situations, but all in all, it’s not something to be used in a real project.

Tab Atkins Jr. blogged about this a few months ago. He wrote a whole proposal about toggling states in CSS. A glance at his work:

toggle-states:  none | <integer> sticky? | infinity  (initial: none)
toggle-group:   none | <ident>  (initial: none)
toggle-share:   none | <selector>#  (initial: none)
toggle-initial: <integer>  (initial: 1)

… where:

  • toggle-states is the basic function that turns toggle functionality on/off. None means it doesn’t toggle, 2 or more implements different states of checked.
  • toggle-group implements the radio-button functionality. If you click an element with toggle-group set to a non-none value, all elements sharing the same toggle-group will be automatically set to first state (off)
  • toggle-share implements the label functionality. When you click an element with toggle-share, it acts as if you’d clicked all the elements it points to.
  • toggle-initial sets the initial state of the toggle

However, he admitted his idea is not perfect and can still face some issues and paradoxes. I won’t go any further into the topic since it would be out of the scope of this article, but I highly suggest you read it. Even if it’s incomplete, it’s still brilliant.

So we’re back to JavaScript. It’s probably the best way to go if you want consistency and reliability. However, if you want to have fun doing things “the pure CSS way”, then you might have found this article useful. 😉

On a side note, Chris Coyier at CSS-Tricks wrote a blog post about handling double clicks with pure CSS about 2 years ago. It may be old, but it’s still very interesting to see some of the ways to get (or fail to get) there.

Anyway, thanks a lot for reading guys. Please be sure to ask questions or show any related work!

Download all examples

Tagged with:

Kitty Giraudel

Non-binary accessibility & diversity advocate, frontend developer, author. Real life cat. They/she.

Stay up to date with the latest web design and development news and relevant updates from Codrops.