DEV Community

M. Andrew Darts
M. Andrew Darts

Posted on

CSS Only Modal using target

Modals are everywhere

Modals are a staple in all web apps and sites. How many times have you pulled for a Javascript library for modals. Or worse, implemented your own. Probably every project you have ever done. I have recently taken the approach of only using Javascript when completely necessary. This has pushed me to explore CSS in a new way. If you need a modal in your web project it is 100% achievable with just CSS. Let's see how!

Target Pseudo Selector

You may be familiar with pseudo selectors like :before, :after, :checked, & :nth-child(n). These are super useful for every day use. There is one pseudo selector you may not be aware of. The way :target works is if the location hash matches an element it will trigger the :target pseudo selector on that element. For instance, if my url is mysite.com#article the element with the id #article will trigger the :target pseudo selector. This may seem very simple, because it is! Let your imagination go with it. There are many things you can build with this knowledge, with no Javascript.

Pro Tip

This :target pseudo selector supports CSS animations and transitions.
This means you can easily animate between states, this gives you quite a bit of power!

Below is an example of an animating modal with just CSS.

Top comments (10)

Collapse
 
artydev profile image
artydev

Great, thank you very much :-)

Collapse
 
skavadias profile image
Stamatis • Edited

Thank you very much for this contribution @deathshadow60.
For some reason, I believe this could be written in a way a bit more self-explanatory and such that it provides independent classes for "modal close on click outside", "modal slide-down/up" and "modal fade-in/out" that are also compossable:

  <style>
    .modal {
      /* The Modal background: main fades toward black */
      background-color: rgb(0,0,0);      /* Fallback color */
      background-color: rgba(0,0,0,0.5); /* Black w/ opacity */
    }
    .modal-click-out-close > a {
      /* Use <a href="#"></a> at first level inside modal */
      position:absolute;   /* Close modal on click outside!!! */
      top:0;
      left: 0;
      /* These are *necessary* for outside area definition */
      height:100%; width:100%;
    }
    /* Slide-down/up _and_ fade-in/out modals: CSS-only! */
    .modal-slide-down-up,
    .modal-fade-in-out {
      display: flex;
      position: fixed;  /* left:-100vw is out of the window */
      /* These are *necessary* for modal (and bg) positioning */
      height:100%; width:100%;
      left: -100vw;
      /* transition of *left edge* in 0s w/ delay 100ms */
      transition: left 0s 100ms;
    }
    .modal-slide-down-up:target,
    .modal-fade-in-out:target {
      display: block;
      left: 0;
      /* transition of *left edge* in 0s */
      transition: left 0s;
    }
    /*****************************************************************
     * .modal-fade-in-out _must_ come after .modal-slide-down-up     *
     * definition, so that it overrides the lack of an _opacity_     *
     * transition in the latter, resulting in two classes that are   *
     *             both *independent* and *compossable*              *
     * We cannot just _add_ the opacity transition; left and opacity *
     * transitions _cannot_ be separated in .modal-fade-in-out and,  *
     * thus, also in the case of composing the two classes.          *
     *****************************************************************/
    .modal-fade-in-out {
      opacity: 0;
      /* transition of *left edge* in 0s w/ delay 100ms */
      /* transition of *opacity* in 100ms */
      transition: left 0s 100ms, opacity 100ms;
    }
    .modal-fade-in-out:target {
      opacity:1;
      /* transition of *opacity* in 100ms */
      transition: opacity 100ms;
    }
    .modal-slide-down-up:target > div {
      top: 150px;
      /* Exact same transition params: no transition required here */
      /* transition: top 100ms; */
    }
    .modal-slide-down-up > div {
    /* transition modal contents (div) to new top, but go       *
     * upwards of (position:relative to) modal initial position */
    position:relative;
    top: -100vh;
    /* transition of *top edge* in 100ms */
    transition: top 100ms;
  }
</style>

<script>
  /* ESC key (key code 27) exits modals */
  document.addEventListener("keyup",
    event => {
      if (event.keyCode === 27) {
        location.href="#";      /* or: location.hash = ''; */
      }
  });
</script>
Enter fullscreen mode Exit fullscreen mode

Very educational, though; thanks.
Nevertheless, it took me days to understand how it works, the way it was written!

Collapse
 
deathshadow60 profile image
deathshadow60 • Edited

Visibility leaves it in place, and prevents you being able to have a fade transition. Instead of visibility:hidden try position:absolute; left:-100vw; as you can then use transition delays to make it fade in/out.

I'd also suggest using selectors instead of blindly throwing classes at everything. The MYTH that "classes for everything" saves render time or other such nonsense is the realm of fantasy not fact. Even if big names like Google say otherwise in ignorance.

Likewise combining like attributes in selectors can help greatly reduce your code bloat, and using absolute positioning on the inner elements can work wonders for behavioral issues like a fade-in / fade-out transition.

It's why that translateY of yours doesn't seem to actually be doing anything... is that supposed to slide in from the top? Because it isn't.

Also remember that px is inaccessible trash that could lead to broken layouts for non-standard font-metric users like myself. (where my laptop and workstation are set to "large / 120dpi / 8514 / 125% / win7+ medium / pick a name already" and my media center is set to 32px thanks to the 4k display) 99.99% of the time you see a width in pixels, or padding in pixels, or font-size in pixels, you're looking at broken inaccessible methodology. It's called EM, use 'em! Much less the use of REM inside a pixel container... no... just ... no.

Hence I'd gut the markup down to:

<div id="greeting" class="modal">
    <a href="#"></a>
    <div>
        <h3>This is a modal</h3>
        <p>This is the modals content.</p>
  </div>
<!-- #greeting.modal --></div>

and then use CSS more along these lines:

/* assumes everything is set to box-sizing:border-box; */

.modal,
.modal > a {
    position:fixed;
    top:0;
    width:100%;
    height:100%;
}


.modal {
    left:-100vw;
    padding:0 2em;
    opacity:0;
    transition:left 0s 0.5s, opacity 0.5s;
    display:flex;
    justify-content:center;
    align-items:center;
    background:rgba(0,0,0,0.2);
}

.modal:target {
    left:0;
    opacity:1;
    transition:left 0s, opacity 0.5s;
}

.modal > a {
    position:absolute;
    left:0;
}

.modal > div {
    position:relative; /* depth sort over anchor */
    top:-50vh;
    width:100%;
    max-width:32em;
    padding:1em;
    background:#FFF;
    border-radius:0.5em;
    transition:top 0.5s;
}

.modal:target > div {
    top:0;
}

Have a look at my own older articles on this, where I use both the :target technique for a modal, and the input:checked technique for mobile menus.

cutcodedown.com/tutorial/modalDialogs
cutcodedown.com/tutorial/mobileMenu

I actually need to update both of those as I've been refining how I use them the past year and a half.

I've been using both methods for both effects interchangeably due to how sometimes you really do want to trigger them one way or the other. The INPUT approach is nice because it doesn't fill up the browser history. I often further enhance with this scripting:

var
    closeAnchors = document.querySelectorAll('.modal > a');

for (var i = 0, a; a = closeAnchors[i]; i++)
    a.addEvenListener('click', anchorBack, false);

function anchorBack(e) {
    e.preventDefault();
    window.history.back();
}

So that you don't fill up the browser history by hitting back. The advantage being you get the bells and whistles of scripted behavior, but it still works when scripting is blocked, disabled, or otherwise unavailable.

If you use the input:checked + div technique for your modals, it leaves the option to use :target for single-page that behaves as multiple pages, which is often handy when creating references. I made a x86 reference for my retrocomputing projects doing precisely that. The scripting is minimal to nonexistent on the page, even though the "pages" of said site are just one single HTML file.

x86.cutcodedown.com/

The only thing the scripting is really doing is locking out old browsers, and changing the window TITLE to match the first numbered heading when a section is navigated to via a link.

Fun stuff, it's absolutely amazing how much we can do faster, cleaner, simpler, and better without a lick of JavaScript.

Collapse
 
mandrewdarts profile image
M. Andrew Darts

So much knowledge in this comment, I really appreciate it!

I agree, I really like the checkbox approach as well. This was just a fun little hack I came across 😁

The transitions are subtle and quick. I can say they are working in Chrome based browsers.

I really appreciate you checking this out and providing feedback! I learned some things 🤘

Collapse
 
deathshadow60 profile image
deathshadow60 • Edited

The fade in transition "kind of" works here in Vivaldi (which is Blink/chromium based) but you have no fade-out animation at all -- because visibility cannot transition. Poof, it's gone.

Neither visibility or display:none are great choices when you want animations. It sucks bad, but that's just how it is.

Your TranslateY didn't seem to do anything I could see in any browser I tested (chromium, vivialdi, edge, firefox) hence why I made a more... exaggerated version with relative positioning that should be more apparent something is happening.

Same issue bit me when I was first starting to try to use these techniques for animations. The key is you're declaring a half second animation -- which is NOT a "quick" transition by any definition -- but you're not getting anywhere near that in the result... and the use of visibility is the cause.

Thread Thread
 
mandrewdarts profile image
M. Andrew Darts

"not working at all" to "kind of" I'll take it.

You are correct, .5s is not a quick transition. What I meant is the curve of the transition is quick.

I also agree that visibility and display are not properties you should be using for CSS animations, I was pretty surprised when it worked with visibility. I imagine it is a browser compatibility issue.

We are getting in the weeds a bit, this was just to show the target technique.

Thread Thread
 
deathshadow60 profile image
deathshadow60 • Edited

We are getting in the weeds a bit, this was just to show the target technique.

... and a very powerful technique it is, hence my giving it a big thumbs up and taking the time to expand upon it. We need to spread the word about these types of things so people can stop throwing JavaScript at things in a way that breaks accessibility.

There's so much we can do without JS now, and things that never should have been scripting's job in the first place that people still just blindly dive for JS to accomplish "the hard way".

Hell, there are still people calling themselves "experts" or "professionals" who will use JavaScript instead of :hover because they don't know enough HTML, CSS, or JS to even be building pages.

See half-witted incompetent trash like jQuery's "$(whatever).fade()". NOT JAVASCRIPT's JOB!

Avoiding using scripting in a broken way, or limiting scripting to enhancing a page instead of being the only means of providing functionality is SO important right now, what with the precedent set in the Domino's case that laws like the US ADA or UK EQA / DDA no longer just applying to medical/utilities/government/banking websites. It's also why things like letting react do render client-side is a walking talking /FAIL/ at development.

We NEED to get the word out that techniques like :target and :checked are how it should be done -- in as cleanly and gracefully degrading a manner as possible -- if for no other reason than for site owners to avoid getting dragged into court.

Thread Thread
 
mandrewdarts profile image
M. Andrew Darts

Yes! I really appreciate the conversation 😁

Collapse
 
johnkazer profile image
John Kazer • Edited

I don't really want my app state spread over CSS, HTML, JavaScript... How would you ensure that "easily animate between states" within CSS doesn't cause confusion and complexity?

Collapse
 
mandrewdarts profile image
M. Andrew Darts

Great point! I would stray away from this in a medium to large size application for the same reason.

This is a nice technique for someone who is doing a simple marketing site that doesn't really have "state".

I would argue that some state is already in CSS. A lot of these are pseudo selectors. :hover, :focus, & :checked are examples of this.

I am not saying this is the "correct" way to do it, but a simple one for a simple use case. It is all based around your needs for your specific project.