Using CSS :target Pseudo-Class To Toggle Element Display
The other day, I was listening - I think - to the ShopTalk Show podcast when they mentioned the :target
CSS pseudo-class. I had never heard of this CSS selector before; but, apparently, it allows you to target an element whose id
attribute matches the URL fragment. This CSS selector is sometimes used as JavaScript-free means to hide and show elements based on the user's interactions. This piqued my curiosity and I wanted to see if I could revamp my recent position: sticky
demo to use the :target
CSS selector to hide and show a user's membership details.
Run this demo in my JavaScript Demos project on GitHub.
View this code in my JavaScript Demos project on GitHub.
In my previous demo, I was using position: sticky
in the horizontal direction to render a timeline of user memberships within an organization. With today's exploration, I want to take that same data, but flip the experience: instead of being able to view all the membership details directly within the timeline, I want the timeline to be abstract and clickable. And, when you click on a user-track within the timeline, I want the relevant membership details to become visible in the aside.
And, I want to do this without any JavaScript.
To do this, I'm going to start by setting all user details to display:none
. Then, I'm going to show any user detail element that matches the current URL fragment via the :target
pseudo-class:
.user {
display: none ;
}
.user:target {
display: block ;
}
And, to make this an even more fun exploration, if no user detail is visible (the default state of the page), I want to show a "call to action" indicating that timeline is clickable. Of course, once a user detail is visible, I want this call-to-action to hide.
To do this, I'm going to be using the general sibling combinator. This is a CSS selector that relates two sibling selectors and targets the second one. So, for example, if we had this combinator:
h1 ~ p
... it would target any <p>
element that followed an <h1>
element within the same parent container.
In my demo, I'm going to start by showing the call-to-action. But then, hide the call-to-action if it follows a .user
that is currently being targeted by the URL fragment:
/*
We're going to be using the :target CSS pseudo-class to hide and show the
user details within our timeline. By default, all the user details will
be HIDDEN and the call-to-action will be SHOWN.
Then, when we change the URL fragment to :target a user detail, we'll
show the user detail and HIDE the call-to-action (using a general sibling
combinator "~").
*/
.user {
display: none ;
}
.call-to-action {
display: block ;
}
.user:target {
display: block ;
}
/* If the call-to-action follows a targeted user, hide it. */
.user:target ~ .call-to-action {
display: none ;
}
As you can see, the default state is to hide all user details and to show the call-to-action. Then, once a user detail is targeted, we show only the given user detail and hide any call-to-action that follows the targeted element. To get this to work, all the user details and the call-to-action have to be in the same parent container, otherwise the general sibling combinator won't work.
I'm not going to show all the data-crunching, since it's kind of complex and not really the point of this post. As such, just assume that we have a "timeline" of "tracks" where each track represents a user. And, within each "track", we have a series of "segments" where each segment represents a membership that a user has to a company.
Here's the Lucee CFML page that renders the timeline - notice that here is no JavaScript - this all being done through CSS:
<!--- Creates the global variable, "timeline". --->
<cfinclude template="./compile.cfm" />
<cfoutput>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>
Using CSS :target Pseudo-Class To Toggle Element Display
</title>
<link rel="stylesheet" type="text/css" href="./styles.css" />
<style type="text/css">
/*
We're going to be using the :target CSS pseudo-class to hide and show the
user details within our timeline. By default, all the user details will
be HIDDEN and the call-to-action will be SHOWN.
Then, when we change the URL fragment to :target a user detail, we'll
show the user detail and HIDE the call-to-action (using a general sibling
selector "~").
*/
.user {
display: none ;
}
.call-to-action {
display: block ;
}
.user:target {
display: block ;
}
/* If the call-to-action follows a targeted user, hide it. */
.user:target ~ .call-to-action {
display: none ;
}
</style>
</head>
<body>
<div class="columns">
<div class="columns__left">
<!-- BEGIN: Tracks. -->
<cfloop index="track" array="#timeline.tracks#">
<!---
When clicking on the track / user link, we're going to set the
FRAGMENT of the URL. This fragment corresponds to the ID value of
the ".user" DIV in the page aside. We're going to be using CSS
selectors (:target) to show the right user based on the FRAGMENT.
--->
<a href="###track.id#" class="track">
<cfloop index="segment" array="#track.segments#">
<span
style="left: #segment.offsetInPercent#% ; width: #segment.durationInPercent#% ;"
class="track__segment">
</span>
</cfloop>
</a>
</cfloop>
<!-- END: Tracks. -->
</div>
<div class="columns__right">
<!-- BEGIN: Details. -->
<div class="details">
<cfloop index="track" array="#timeline.tracks#">
<!---
Notice that our ID attribute here is the same as the HREF in
track above. By default, all of these elements will be hidden.
Then, we'll conditionally show the one that matches the
current URL fragment.
--->
<div id="#track.id#" class="user">
<h3>
#encodeForHtml( track.user.name )#
</h3>
<ul>
<cfloop index="segment" array="#track.segments#">
<li>
#segment.startedAtLabel# to #segment.endedAtLabel#
</li>
</cfloop>
</ul>
<a href="##">Close</a>
</div>
</cfloop>
<div class="call-to-action">
← Click on tracks to view details.
</div>
</div>
<!-- END: Details. -->
</div>
</div>
</body>
</html>
</cfoutput>
As you can see, when I am layout-out the tracks, each track is just an <a>
tag that points to:
<a href="###track.id#" class="track">
This HREF will change the window location fragment to contain the track.id
value. Then, when we layout the user detail elements, notice that each user has an id
that matches the track:
<div id="#track.id#" class="user">
Since the id
of the detail matches the href
of the track, we can use the :target
CSS pseudo-class selector to conditionally hide and show the details. And, when we run this ColdFusion page, we get the following browser behavior:
How cool is that?! As you can see, when the URL fragment is empty, we show the call-to-action. Then, when the URL fragment is populated, we hide the call-to-action and show the targeted user detail.
When I have to create a rich, interactive page my default response is to reach for JavaScript; and, more specifically, for Angular. And, in many cases, this is a perfectly reasonable response. But, it's nice to know that, sometimes, we can create some interactive behaviors using just CSS. In this case, I am using the :target
CSS pseudo-class to conditionally show elements that match the URL fragment / hash.
Want to use code from this post? Check out the license.
Reader Comments
That is so awesomely clean. I love that we can do this with CSS and yet again, I did not have a clue about the pseudo selector:
This feels like going to confessional. But, I guess I wouldn't find this stuff interesting, if I knew all about it, before hand!
Anyway, it is great having a CSS alternative to the JS hide/show functionality.
@All, @Charles,
I've been told on the Twitter that using
:target
to changedisplay
CSS can cause accessibility (a11y) issues for assistive devices. I'm pretty new to thinking about accessibility, so things are not yet obvious to me.Just a word of caution!
@Charles,
That said, using the
:target
CSS to modify styling (likebackground-color
) I believe is completely fine.