We know CSS is used to style a UI component, but did you know that you can add interactivity with just CSS as well?
In this post we are going to find out how.
JavaScript is great — the purpose of this post is not to make the case that you don’t need JavaScript at all. However, it’s good to be aware that you can build many UI components functionality without the additional dependency of JavaScript.
Let’s take a look at some patterns that will help us to add interactivity to our UI components with just CSS.
In HTML, we can establish relationships between a link and an element with id
attribute of the element along with href
in an <a>
tag.
Here’s an example:
<a href="#p1">Jump to the paragraph</a> <p id="p1">Hello! I'm a paragraph.</p>
If we click on the link, the page will automatically jump to the paragraph with id="p1"
because the href
of <a>
matches with it. We now have a relationship between these two elements.
Now, if we want to modify styles or add additional styles when <p id="p1">
is in focus, we can use the :target
pseudo-class. :target
helps us in targeting the element with an id
matching the URL’s fragment i.e the href
.
p:target { background-color: thistle; }
Now when the page jumps to the paragraph after the link is clicked, the paragraph will also have a background color as mentioned.
Let’s see some examples of UI components using this pattern.
CSS only modal/dialog
No Description
Here’s the simplified versions of the HTML and CSS:
<!-- #dialog creates the relationship between the button link and dialog --> <a class="button" href="#dialog">Click To Open Modal</a> <!-- wrap dialog body in "overlay" --> <!-- role="dialog" will let the screen reader know the purpose of this section --> <div id="dialog" class="overlay" role="dialog"> <div class="dialog-body"> <!-- href="#" resets the target, hence the dialog closes --> <a class="close" href="#" aria-label="Close dialog">×</a> <!-- content --> </div> </div> .overlay { /* Positioning and styling of the overlay */ position: absolute; top: 0; bottom: 0; left: 0; right: 0; background: rgba(0, 0, 0, 0.8); transition: opacity 250ms; /* Dialog is hidden until it matches an anchor target */ visibility: hidden; opacity: 0; /* Dialog fades in when it matches an anchor target */ &:target { visibility: visible; opacity: 1; } } .dialog-body { position: relative; margin: 50px auto; /* center the dialog body */ width: 300px; background: #fff; /* positioning the close button inside the dialog body */ .close { position: absolute; width: 20px; height: 20px; top: 20px; right: 20px; } }
Caveats: you cannot close the dialog with esc
key, and you cannot trap focus inside the dialog.
Css only lightbox image viewer
No Description
Here’s a simplified version of the code:
<!-- thumbnail image --> <a href="#img1"> <img src="https://picsum.photos/id/999/300/300" alt="description of image"> </a> <!-- lightbox image (visually hidden) --> <div class="lightbox" id="img1"> <a href="#" class="close" aria-label="close image">×</a> <img src="https://picsum.photos/id/999/800/400" alt="description of image"> </div> .lightbox { /* Hidden lightbox image */ display: none; position: fixed; z-index: 999; width: 100%; height: 100%; text-align: center; top: 0; left: 0; background: rgba(0, 0, 0, 0.5); img { /* Add some padding to the image when enlarged */ max-width: 90%; max-height: 80%; margin-top: 2%; } &:target { /* Show the lightbox */ outline: none; display: block; } } .close { position: absolute; top: 20px; right: 20px; font-size: 3em; color: #fff; text-decoration: none; }
Some caveats: both images are loading at all times. To resolve this additionally with JavaScript, we can load the larger size of image only when the thumbnail is clicked.
The thing about radio buttons is that in a group of radio buttons that has the same name
attribute, only one of them can be checked
.
We can use this to our advantage to create UI components that work on this principle, i.e, only one section can be selected at a given instance (such as Tabs and Accordions).
Since these components are radio buttons, you can readily navigate them with arrow keys. No extra setup is needed.
CSS only tabs
No Description
Simplified version of the code:
<div class="tabs" role="tablist"> <!-- all the radio buttons has name i.e radioTab so at a time only one can be checked --> <div class="tab" role="tab"> <input type="radio" name="radioTab" id="tab-1" checked> <label for="tab-1">Label 1</label> <div class="content"> Content for label 1 </div> </div> <div class="tab" role="tab"> <input type="radio" name="radioTab" id="tab-2"> <label for="tab-2">Label 2</label> <div class="content"> Content for label 2 </div> </div> <div class="tab" role="tab"> <input type="radio" name="radioTab" id="tab-3"> <label for="tab-3">Label 3</label> <div class="content"> Content for label 3 </div> </div> </div> .tabs { position: relative; display: flex; align-items: flex-start; min-height: 200px; /* give a min height, can be anything you want */ } .tab { display: flex; flex-direction: column; label { background-color: #0094a7; } /* hide the radio buttons visually*/ [type="radio"] { position: absolute; height: 0; width: 0; overflow: hidden; clip: rect(0, 0, 0, 0); /* change color of active tab */ &:checked ~ label { background: #007584; } /* makes the active tab's content visible */ &:checked ~ label ~ .content { opacity: 1; z-index: 1; /* increase the z-index so the content is in focus*/ } } } .content { position: absolute; left: 0; right: 0; bottom: 0; top: 3em; width: 100%; height: 100%; z-index: -1; opacity: 0; /* hides the tab content by default */ }
CSS only accordion
No Description
Simplified version of the code:
<div class="accordion"> <!-- all the radio buttons has name i.e radioPanel so at a time only one can be checked --> <div> <input type="radio" name="radioPanel" id="panel-1" checked> <label for="panel-1">Panel 1</label> <div class="content"> Content for Panel 1 </div> </div> <div> <input type="radio" name="radioPanel" id="panel-2"> <label for="panel-2">Panel 2</label> <div class="content"> Content for Panel 2 </div> </div> <div> <input type="radio" name="radioPanel" id="panel-3"> <label for="panel-3">Panel 3</label> <div class="content"> Content for Panel 3 </div> </div> </div> .accordion { /* Visually hide the radio input */ input[type="radio"] { position: absolute; height: 0; width: 0; overflow: hidden; clip: rect(0, 0, 0, 0); &:checked ~ .content { max-height: 20em; /* give a max height, can be anything */ } &:checked + label { background-color: #3aa13a; } } label { position: relative; display: block; background-color: darkseagreen; color: white; transition: all 300ms ease-in-out; cursor: pointer; } .content { /* so that the content is scrollable when it exceeds the mentioned max height*/ overflow: auto; max-height: 0em; position: relative; transition: all 300ms ease-in-out; } }
With the attr()
CSS function, we can define content in our html markup using any custom property and then fetch the value.
This currently only works with the content
property. Using a combination of these, we can create tooltips that have dynamic content in them.
CSS only tooltip with attr()
No Description
<p> <span data-title="Hello in french">Bonjour!</span> How are you? </p> span { position: relative; color: blue; cursor: pointer; border-bottom: 2px dotted currentcolor; &:before { /* content inside will come from the data-title attribute as defined in HTML */ content: attr(data-title); opacity: 0; position: absolute; top: 30px; right: -90px; font-size: 14px; width: 100px; padding: 10px; color: #fff; background-color: #555; border-radius: 3px; pointer-events: none; } &:hover:before { opacity: 1; } }
In the CodePen example, I have used aria-label
to access the content.
In the example above, I used data-title
, this is just to show that attr()
can access the content from any custom property.
These are just few examples that show how interactivity can be achieved with just CSS.
The next time you’re building a component, consider how much of it can be achieved just with CSS without bloating our JavaScript files.
Here are a few other awesome examples from around the web:
As web frontends get increasingly complex, resource-greedy features demand more and more from the browser. If you’re interested in monitoring and tracking client-side CPU usage, memory usage, and more for all of your users in production, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording everything that happens in your web app, mobile app, or website. Instead of guessing why problems happen, you can aggregate and report on key frontend performance metrics, replay user sessions along with application state, log network requests, and automatically surface all errors.
Modernize how you debug web and mobile apps — start monitoring for free.
:target
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowEfficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.
Design React Native UIs that look great on any device by using adaptive layouts, responsive scaling, and platform-specific tools.
Angular’s two-way data binding has evolved with signals, offering improved performance, simpler syntax, and better type inference.
Fix sticky positioning issues in CSS, from missing offsets to overflow conflicts in flex, grid, and container height constraints.