Building CSS only interactive components from scratch - Part I (modal window)

JoelBonetR 🥇 - May 29 '20 - - Dev Community

*Each post about Building CSS Only interactive components is stand alone, so you don't need to understand part 1 to understand the part 2. Feel free to check it in the order you want, need or like.
Also a basic understanding about how CSS works is required to understand the workarounds of this posts. If you want to level up your CSS acknowledgement, you can visit this post .


  1. Building CSS Only interactive components from scratch PART 1
  2. Building CSS Only interactive components from scratch PART 2
  3. Building CSS Only interactive components from scratch PART 3

Hi there,

I'll make a post series about components that usually people code using javascript but... well, without javascript. Using CSS Only to make them more efficient to load and render.

The main point on this post and for the task I'm showing is the usage of the :target pseudo-selector.

Without further delay let's take a dive into it.

The HTML code:


<div id="open-modal" class="modal-window">
  <div class="modal-content">
    <a href="#!" title="Close" class="modal-close"> X </a>
    <h2>Yaaay!</h2>
    <div>
      <p class="bold">A nice responsive css only modal.</p>
    </div>
    <div>
      <p>
        You can close this dialog by clicking the close button at the bottom, the cross at the top right or the backdrop.
      </p>
      <p>
        Lorem ipsum dolor sit amet consectetur adipisicing elit. Deleniti dolore repellat nulla sunt, sed libero voluptatem facilis saepe iste dolores et dignissimos ea fugiat sit quae aliquam nisi corporis tempora.
      </p>
    </div>
    <div class="modal-footer">
      <a href="#!" title="Close" class="btn modal-close"> Close </a>
    </div>
  </div>
</div>
<a class="modal-backdrop" href="#!"></a>
<a class="btn modal-btn" href="#open-modal" name="a">
  Open Modal
</a>

Remember that Dev.to renders html "as is" and i didn't find a way to avoid it so I put white spaces plus a hyphen on each html tag as an ugly way to reach it.

Now we have the structure which is:

  • a modal window
    • a modal content
      • a close cross icon ( × )
      • a modal title (actually a h2 tag because you will (or should) get an h1 already on main content.
      • some content (you can put whatever here)
    • a modal footer
  • a modal backdrop (this darken background that usualy shows behind the modal window)
  • the button that will trigger the modal.

At this point it only seems a mess if you try to render it because of course, we need styles.

!important: All styles here will be written in SASS/SCSS which is preferred due to maintainability of it versus plain CSS.

step one:

.modal-btn { 
  font-size: 20px; 
  font-weight: bold; 
  text-decoration: none; 
  color: #282828; 
  border-radius: .4em; 
  border: 1px solid lightblue; 
  height: fit-content; 
  padding: 5px 15px;
  &:hover, &:hover p {
    background: lightblue;
  }
}

This only gives a little shape to the "open modal" button, you can build what you like but it needs to be a link html tag ( <a> ) below you'll see why.

.modal-window { 
  position: fixed; 
  left: 50%; 
  top: 50%; 
  right: 50%; 
  bottom: 50%; 
  transform: translate(-50%, -50%); 
  z-index: 999;
  visibility: hidden; 
  opacity: 0; 
  pointer-events: none; 
  transition: opacity 0.25s;

Here we set the modal-window as hidden, setting it fixed to the middle of the screen, disabling the pointer-events (must not be necessary but depending the layout could cause a miss-click on a hidden element so it's preferred) and applying a transition that will apply on opacity change.

*Note that i didn't close the .modal-window selector, this is because we will add more content here. In fact, all the code that left will be wrapped inside .modal-window selector

  &:target { 
    visibility: visible; 
    opacity: 1; 
    pointer-events: auto; 
    +.modal-backdrop { 
      background-color: rgba(0, 0, 0, 0.75); 
      position: fixed; 
      top: 0; 
      right: 0; 
      bottom: 0; 
      left: 0; 
      z-index: 990;
    }
  }

Now I'm using :target pseudo-selector inside .modal-window. The & means an addition, so it will translate into:
.modal-window:target when parsed into plain CSS.

What means target pseudo-selector?

An element will be targeted (:target = true) when a hash with it's ID is on the URL.

Example:
Base URL:
https://myawesomeproject.com/
Base URL + hash
https://myawesomeproject.com/#about-us

This means that, if there's an html element with the id attribute with "about-us" as value (i.e. div id="about-us" ), it will be targeted.

Now you can understand why the "open modal" button must be a link tag instead a button, and why the href attribute on it is the same than the ID of the modal-window.

After this point, let's take it back to the main thread. What I'm doing when .modal-window is :target?

Making .modal-window visible, increasing the opacity to 1 (which means 100%) and enabling all pointer-events on it.

In addition, we need to show the backdrop too, so there's the sibling selector + pointing to the .modal-backdrop, where we set some properties to make it visible and positioned behind the modal (z-index 990 while .modal-window has 999).

Note that backdrop is an <a> tag too, that points to #! as well as close button on modal-footer and the × on the top right of the modal.

  &>div.modal-content { 
    width: 900px; 
    max-width: 90vw; 
    position: fixed; 
    top: 50%; 
    left: 50%; 
    transform: translate(-50%, -50%); 
    padding: 10px 30px; 
    background: #fff; 
    max-height: 90vh; 
    overflow-y: auto;
  }

Properties for the specific content container on the modal.
Fixed to the centre, with max height and width properties and scrollable if needed.

 .modal-close:not(.btn) { 
    position: fixed; 
    right: 15px; 
    top: 5px; 
    font-size: 1.2rem; 
    text-decoration: none; 
  }
  .modal-close, .modal-close:visited { 
    color: #212121; 
  }
  .btn.modal-close { 
    font-size: 1.2rem; 
    text-decoration: none; 
    border: 1px solid #333; 
    padding: 10px 15px; 
    background: #fff; 
    border-radius: .4em; 
  }
  .btn.modal-close:hover { 
    background: #f0f0f0; 
  }

These are styles for the × and the close buttons (actually there are links, for the same reason I told you before).
As you can see the close <a> tags have href="#!". This removes any target without reloading the page (which will happen with a void href attribute).

  .modal-footer { 
    width: 100%; 
    display: flex; 
    justify-content: flex-end; 
    margin: 15px 0px 0px 5px;
  }
}

Finally the modal-footer and the closing bracket for .modal-window, with the close button aligned to the right and with a bit of margin.

All together will look like this:

.modal-btn { 
  font-size: 20px; 
  font-weight: lighter; 
  text-decoration: none; 
  color: #282828; 
  border-radius: .4em; 
  border: 1px solid lightblue; 
  height: fit-content; 
  padding: 5px 15px;
  &:hover, &:hover p {
    background: lightblue;  
  }
}
.modal-window { 
  position: fixed; 
  left: 50%; 
  top: 50%; 
  right: 50%; 
  bottom: 50%; 
  transform: translate(-50%, -50%); 
  z-index: 999;
  visibility: hidden; 
  opacity: 0; 
  pointer-events: none; 
  transition: opacity .25s;
  &:target { 
    visibility: visible; 
    opacity: 1; 
    pointer-events: auto; 
    +.modal-backdrop { 
      background-color: rgba(0, 0, 0, 0.75); 
      position: fixed; 
      top: 0; 
      right: 0; 
      bottom: 0; 
      left: 0; 
      z-index: 990;
    }
  }
  &>div.modal-content { 
    width: 900px; 
    max-width: 90vw; 
    position: fixed; 
    top: 50%; left: 50%; 
    transform: translate(-50%, -50%); 
    padding: 10px 30px; 
    background: #fff; 
    max-height: 90vh; 
    overflow-y: auto;
  }
  .modal-close:not(.btn) { 
    position: fixed; 
    right: 15px; 
    top: 5px; 
    font-size: 1.2rem; 
    text-decoration: none; 
  }
  .modal-close, .modal-close:visited { 
    color: #212121; 
  }
  .btn.modal-close { 
    font-size: 1.2rem; 
    text-decoration: none; 
    border: 1px solid #333; 
    padding: 10px 15px; 
    background: #fff; 
    border-radius: .4em; 
  }
  .btn.modal-close:hover { 
    background: #f0f0f0; 
  }
  .modal-footer { 
    width: 100%; 
    display: flex; 
    justify-content: flex-end; 
    margin: 15px 0px 0px 5px;
  }
}

There are loads of styles that you can edit as you like to add your own flavour.

What about responsiveness?
There are two properties for div.modal-content that are doing the trick:

    width: 900px; 
    max-width: 90vw;
    max-height: 90vh;

This will make the modal show at 900px by default on desktop and 90% of window height on any resolution as maximum (default height is auto, it will not grow more than needed vertically, but if it grows more than 90% of viewport height, it will cause a natural and vertical scroll.
When you are on a device with a width less than 900px, max-width will take effect, avoiding horizontal scroll. Setting the modal max-width at 90% of viewport width.


You can find the complete code here on codepen. Fork it to use codepen as playground for editing it. If you already have a codepen account simply press Control + S (Command + S on Mac) and it will be forked into your dashboard.

Footnotes

Hope you learnt something with this article, I'll try to post next parts ASAP with new components that usually you find done with javascript, but without javascript. Stay tunned!

Best regards

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .