Written by Ananya Neogi✏️
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.
How to add interactivity with CSS
Let’s take a look at some patterns that will help us to add interactivity to our UI components with just CSS.
When two separate elements need to work together
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
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
- Cannot close the dialog with
esc
key. - Cannot trap focus inside the dialog.
Lightbox style image viewer
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;
}
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.
Radio button controlled UI components
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
CSS only tabs
CSS-Only Tabs Ilvermorny If you fancy living a Hogwarts-style life across the pond. The founder of Ilvermorny, Isolt Sayre, always wished she could go to Hogwarts, and the school definitely seems to embody some of its traditions. Hogwarts Because you love it.
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
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;
}
}
Dynamic CSS-only tooltip
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.
<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.
Conclusion
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:
- Mine Game – A game built purely with CSS 🤯
- Page scroll indicator
- CSS only Countdown Clock– made with HAML & CSS –Examples of CSS-only Image Sliders
References
- Radio-Controlled Web Design
- MDN reference on
:target
Editor's note: Seeing something wrong with this post? You can find the correct version here.
Plug: LogRocket, a DVR for web apps
LogRocket is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.
Try it for free.
The post CSS only components appeared first on LogRocket Blog.
Top comments (0)