CSS Only Modal using target

mandrewdarts profile image M. Andrew Darts ・1 min read

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.

Posted on by:


markdown guide

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>
        <h3>This is a modal</h3>
        <p>This is the modals content.</p>
<!-- #greeting.modal --></div>

and then use CSS more along these lines:

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

.modal > a {

.modal {
    padding:0 2em;
    transition:left 0s 0.5s, opacity 0.5s;

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

.modal > a {

.modal > div {
    position:relative; /* depth sort over anchor */
    transition:top 0.5s;

.modal:target > div {

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.


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:

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

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

function anchorBack(e) {

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.


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.


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 🀘


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.

"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.

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.

Yes! I really appreciate the conversation 😁


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?


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.