Skip to main content
Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.

Using CSS :target Pseudo-Class To Toggle Element Display

By Ben Nadel on

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">
						&larr; 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:

Elements being shown and hidden using :target CSS pseudo-class.

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.



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:

:target

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.

Reply to this Comment

@All, @Charles,

I've been told on the Twitter that using :target to change display 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 (like background-color) I believe is completely fine.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Blog
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.