Ananya Neogi A frontend developer focusing on creating digital products with great user experience.

CSS-only components

5 min read 1483

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.

We made a custom demo for .
No really. Click here to check it out.

CSS-only Modal/Dialog

CSS only modal/dialog

Dialog is hidden until it matches an anchor target */ .overlay { position: absolute; top: 0; bottom: 0; left: 0; right: 0; background: rgba(0, 0, 0, 0.8); transition: opacity 250ms; visibility: hidden; opacity: 0; /* Dialog fades in when it matches an anchor target */ &:target { visibility: visible; opacity:

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">&times;</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.

Lightbox-style image viewer

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">&times;</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.

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

CSS only accordion

CSS-Only Accordion 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="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.

CSS only tooltip with attr()

ISRO is the space exploration body of India. Its vision is to “harness space technology for national development while pursuing space science research & planetary exploration”. NASA is an independent agency of the United States Federal Government responsible for the civilian space program, as well as aeronautics and aerospace research.

<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:

You come here a lot! We hope you enjoy the LogRocket blog. Could you fill out a survey about what you want us to write about?

    Which of these topics are you most interested in?
    ReactVueAngularNew frameworks
    Do you spend a lot of time reproducing errors in your apps?
    YesNo
    Which, if any, do you think would help you reproduce errors more effectively?
    A solution to see exactly what a user did to trigger an errorProactive monitoring which automatically surfaces issuesHaving a support team triage issues more efficiently
    Thanks! Interested to hear how LogRocket can improve your bug fixing processes? Leave your email:

    Is your frontend hogging your users' CPU?

    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.https://logrocket.com/signup/

    LogRocket is like a DVR for web apps, recording everything that happens in your web app or site. 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 apps — .

    References

    1. Radio-Controlled Web Design
    2. MDN reference on :target
    Ananya Neogi A frontend developer focusing on creating digital products with great user experience.

    Leave a Reply