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:

Tiny break: 📬 Want to stay up to date with frontend and trends in web design? Subscribe and get our Collective newsletter twice a tweek.

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

Kitty Giraudel

Non-binary trans accessibility & diversity advocate, frontend developer, author. Real life cat. She/her.

Stay in the loop: Get your dose of frontend twice a week

Fresh news, inspo, code demos, and UI animations—zero fluff, all quality. Make your Mondays and Thursdays creative!

Feedback 34

Comments are closed.
  1. Nice roundup Hugo 🙂

    I agree with people who say that “events” should be handled with javascript, after all, CSS = Cascading STYLE sheets, it’s about style and layout and the looks of a website.

    I’ve seen Tab Atkins’ proposal about a month ago, and personally I would like to have such an option in CSS, BUT, I would probably still keep the event handling for javascript, for me html = content, css = style, and javascript = interactions (including events) still, and separating the three is better for maintenance, especially when you’re working with progressive enhancement.

    Anyway good work on this article. 🙂

    • Completely true. I took an old example of checkbox hack and didn’t think about changing the way the checkbox is hidden.

    • After a quick glance at other solutions to hide the checkbox it seems that:
      – Jeffrey Zeldamn’s method is only efficient when you want to move text off screen.; in our case, it doesn’t work with the checkbox
      – The display: none method won’t work on Mobile Safari since the label won’t trigger the checkbox anymore
      – Both visibility: hidden and opacity: 0 methods will work if you don’t mind the gap let by the element which is still here

      So I guess we will stick with the good ol’ position: absolute method.

    • What about using clip instead of left + top?

      clip: rect(1px 1px 1px 1px); clip: rect(1px, 1px, 1px, 1px); position: absolute;

      There shouldn’t be any performance hit.

    • This sounds like a very interesting solution, indeed. I’ve been looking at the browser support for the CSS clip() property: Chrome, Firefox, Safari, IE4+ with an old syntax, IE8+ with the correct syntax and Opera 7+. So it looks really great!

      However I have no idea about mobile browser support. If anyone can enlighten me on this stuff, it would be very kind. 🙂

    • I’ve tested with Android 2.3, Opera mobile and Opera mini, and I’ve had no problem. I have not an iPhone or similar, so I can’t test with Safari, but I think there won’t be any problems.

    • There is another reason why positioning elements outside the viewport is not a good idea:

      If you view your first example in firefox, and make the browser viewport small enough so you need to scroll to he “click me!” text and click it, you’ll notice that the browser scrolls to the top of the page.

      This is because FireFox is focusing the checkbox and is trying to scroll the checkbox it into view.

      If you’re going to use this method on a page —or in an element — that scrolls, then you’re going to encounter this problem.

  2. That transition hack is an unbelievably creative use of CSS.

    Regarding the checkbox hack; consider an ideal world where browser support is universal – would you not consider it to be a legitimate solution for handling click events?

    Obviously, that’s not strictly using radio buttons as they were intended to be used, but if you can see past that (which I think we should), then it seems a perfectly viable option to me.

    What are your thoughts?

    • Would visibility:hidden; and position:absolute work? Visibility hides the element and position absolute gets rid of the gap.

  3. This blog post is about showing the possibilities of CSS and some clever hacks. It’s clearly not for “real life” use cases.
    Then what’s the point? Doesn’t this make it a useless waste of time?

    • No!
      Maybe you forgot to read the remaining part of the sentence “..Please, consider the whole article as a playground for experimenting..”

  4. Cool. Nice round up.
    It’s always great to see all the possibilities of CSS, practical or not.

  5. Really cool post!!
    Great hacks that I didn’t knew and others that I’ll never imagine. Thanks!

  6. You can alter more than one element using the :target hack if you put the target id on a element wrapping all the affected elements and then simply use descendant selectors such as “#target:target .affected-element-1”, “#target:target .affected-element-2”. The most general solution would be to wrap your whole page with one nested div for each targetable id, not really pretty though but sometimes useful for mockups.

  7. Hi..
    Thank you for making the only actual tutorial that I’ve been able to find. Other ‘tutorials’ haven’a actually explained how to do things, but you did so, very clearly.
    Thanks..

  8. Wouldn’t been easier if we just put the input to display none since the label attached to it would still work?.

  9. When i added the INPUT and LABEL inside the DIV or Class, the check box hack doesn’t works… please help to find a solutions

    ——————
    CheckBox Hacks – Not Works for me!
    ——————

    Do Something

    /div>

    Control me

  10. Thank you so much for this article! Exactly what I needed. Seriously cannot thank you enough.

  11. First of all, I want to apologize for my English, I do it as well as I can, but I am not English so…

    Well, I think that this article is simply awesome. I am in a forum community where the use of JavaScript is not allowed so I’ve been wondering for this kind of articles for a long time. Yes, I know that we don’t have to use these hacks, but for some things that are not important, such as some boxes with cool display and stuff like this is quite useful.

    But I’m not here only to thank all your work. I’ve come just to say something more:
    ÂżWhy don’t we use the checkbox hack with radio buttons? Using this way, it could be possible to create the click of the mouse with self-changing displays as long as only one radio button can be “:checked” at each time.
    ÂżDo you understand what I mean?

    I don’t know whether it can be useful or not but I have used a lot so, for me, it is quite useful. I don’t know if for the others it could be too.

    Thanks for reading ^^