DEV Community

Cover image for CSS only components
Brian Neville-O'Neill
Brian Neville-O'Neill

Posted on • Originally published at blog.logrocket.com on

CSS only components

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>
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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.

LogRocket Free Trial Banner

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">&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;
      }
    }
Enter fullscreen mode Exit fullscreen mode

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">&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;
    }
Enter fullscreen mode Exit fullscreen mode

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 */
    }
Enter fullscreen mode Exit fullscreen mode

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;
      }
    }
Enter fullscreen mode Exit fullscreen mode

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;
      }
    }
Enter fullscreen mode Exit fullscreen mode

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:

References


Editor's note: Seeing something wrong with this post? You can find the correct version here.

Plug: LogRocket, a DVR for web apps

 
LogRocket Dashboard Free Trial Banner
 
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)