5 CSS-only solutions for common web-dev tasks

Maciej Baron
Good Praxis
Published in
8 min readSep 8, 2022

HTML and CSS has evolved quite a lot in recent years. I still remember the times when you had to remember browser-specific quirks, create different hacks and apply a bunch of polyfills to make websites work consistently across platforms.

Nowadays, things are relatively easier to manage, and, with a couple notable exceptions (looking at you Safari!), expect the same or similar outcome across browsers. Internet Explorer is dead, the beast has been slayed. The majority of internet users use browsers which auto-update. We can finally focus more on building rather than hacking.

There used to be a time when jQuery was used across most websites. It was a good framework that provided cross-browser abstractions, query selector functionality and convenience functions for common problems. However people would often include the library even for the simplest of tasks. Because of this, especially since the introduction of Selectors API in 2013, there’s been a push to use vanilla JavaScript to improve performance, reduce the number of dependencies and make developers become more familiar with the underlying programming language powering jQuery.

However, developments in HTML and CSS now allows us to take things even further, and avoid using JavaScript completely. Here are five examples of common dev tasks that can be done without the use of JS.

Accordion

The concept of an accordion is quite simple. You click on it, it opens. You toggle it again, it closes. If you have a list of accordions, you want to toggle them individually, being able to have them open and closed.

You might think to yourself: surely we need JavaScript for such interactions? How do you change and retain the state of individual elements with just CSS and HTML? Surely this can only be achieved by clicking and toggling a class.

The truth is, you don’t need JavaScript. In fact, you don’t even need CSS. You can achieve it by simply using HTML. The often ignored <details> element provides the functionality we need:

<details>
<summary>Our item</summary>
Our item description
</details>

…and that’s it. No, seriously. Now you click on the item header, and it shows the description below it.

Of course this does not really look like the accordions we are used to seeing, but you can easily apply some CSS to make it look more presentable. Here’s a recent example of what we at Good Praxis created for one of our clients:

Accordion list at wearesettle.org

You should be able to recreate such an example easily. Bear in mind that the arrow element is actually controlled by list-style-type on the <details> element.

Tooltip

Sometimes you might want to create an inline tooltip: you hover over a phrase, and a little tooltip shows up providing an explanation. It might look something like this:

To achieve this, we will use three things: attr(), the ::after pseudo-selector and aria-label.

First, let’s look at the HTML:

<p>Lorem ipsum, <span class="with-tooltip" aria-label="Dolor sunt modi tenetur">dolor</span> sit amet consectetur adipisicing elit. Quibusdam provident sunt expedita ut, laudantium officiis quidem modi quisquam asperiores nisi voluptatum nihil neque adipisci laborum nesciunt tenetur, sit enim dignissimos.</p>

We have a simple paragraph with a span element wrapping the word/phrase we want to have a tooltip for. We add a class called with-tooltip and define aria-label with our tooltip text.

Now, let’s write the CSS:

.with-tooltip {
position: relative;
text-decoration: underline;
text-decoration-style: dashed;
cursor: help;
}
.with-tooltip::after {
display: inline-block;
content: attr(aria-label);
opacity: 0;
position: absolute;
width: 5rem;
font-size: .8em;
transition: opacity .5s;
padding: .5em;
background-color: lightyellow;
border: 1px solid black;
border-radius: .5em;
pointer-events: none;
}
.with-tooltip:hover::after {
opacity: 1;
}

In the above code we add a ::after pseudo-element, which is invisible by default. We then set its content to the value of aria-labelby using attr() — this allows us to grab the value of any attribute of the element. The rest is self-explanatory — when we hover over the element, we set the opacity of the tooltip to 1. Since it has position set to absolute, and the left/top values are set to auto, it displays right next to the word. We can make adjustments to this by applying a transform if we want to.

Dropdown menu

We wrote a whole article about how to create a CSS-only dropdown menu, so check it out! You should be able to create something like this:

The solution uses a combination of sibling selectors, the pointer events property and other techniques, which we explain in detail.

It’s also fully accessible and supports keyboard navigation.

Modal

If you read the aforementioned article, you’ll definitely now see how powerful subsequent-sibling combinators can be.

We can use the same technique to create a modal that appears upon clicking a button, and then disappears after clicking a close button — all without JavaScript.

Consider the following markup:

<div class="modal">
<div class="modal-opener" role="button" tabindex="0">Open</div>
<div class="modal-content" tabindex="0">Content</div>
<div class="modal-closer" role="button" tabindex="0">Close</div>
</div>

Let’s assume that model-content is a box that is in the middle of the screen, and has position set to fixed so it gets displayed on top of everything else:

.modal-content {
position: fixed;
height: 100px;
width: 100px;
top: calc(50% - 50px);
left: calc(50% - 50px);
opacity: 0;
pointer-events: none;
}

Now let’s make our closer button also be displayed in the middle of the screen, on top of the modal content box:

.modal-closer {
position: fixed;
top: calc(50% - 50px);
left: calc(50% + 15px);
opacity: 0;
pointer-events: none;
display: inline-block;
background: grey;
}

You probably noticed that both the closer and the content have opacity set to 0, and pointer events set to none. This means they are invisible and cannot be interacted with.

Finally, let’s add some extremely basic styling to our opener so it looks at least a bit like a button:

.modal-opener {
cursor: pointer;
background: grey;
display: inline-block;
}

So far so good. So how do we make it show when we click the button, stay open, and close when we click off, or click the close button?

You guessed it — sibling combinators. See the code below:

.modal-opener:focus+.modal-content,
.modal-opener:focus~.modal-closer,
.modal-content:focus-within,
.modal-content:focus-within+.modal-closer
{
opacity: 1;
pointer-events: initial;
}

The first line uses a next-sibling combinator, pointing at modal content when the modal opener is focused on. The second line uses a subsequent-sibling combinator to select the modal closer, again, when the modal opener is focused on. The third line selects the modal content when there is focus within the element, and finally the last line uses a next sibling combinator to select the closer when there is focus within the modal content.

Let’s look how this works in action. Here’s our primitively styled code:

Obviously we can make it look a lot better…

We kept code quite simple and rudimentary so you can understand what’s going on, but you can easily make it look something like this:

Modal example by Mike Griffin

…and once again, not a single line of JavaScript was used.

You might be wondering why we didn’t use a <button> element. Well, Safari’s the new Internet Explorer… and onBlur/focus does not get fired when clicking on a button, which apparently is working as intended. Go figure.

Drag and drop: puzzle pieces

I know — I said I’d show to common tasks, but I thought we could also have a look at some less useful but nonetheless cool things we can do using CSS only.

Did you know we can create a simple drag and drop mechanic? Let me show you have to do something like this:

Drag and drop in action

To achieve this, we will be abusing the resize property.

You might be familiar with that little symbol in the bottom right corner of the puzzle piece — it’s what you see when you have a textarea and are able to resize it:

Ye’ old <textarea>

This alone should already give you an idea about how this was done. The resize property is something that we can actually assign to any container, as long as overflow is not set to visible.

Consider the following mark-up:

<div class="puzzle">
<div class="piece">
<div class="piece-internal">

</div>
</div>
</div>

We have our main puzzle container, a puzzle piece, and then another container to house the piece itself.

.puzzle {
position: relative;
}
.piece {
width: 100px;
position: absolute;
height: 100px;
resize: both;
overflow: hidden;
}
.piece-internal {
position: relative;
width: 100%;
height: 100%;
}
.piece-internal::after {
position: absolute;
bottom: 0;
right: 0;
width: 100px;
height: 100px;
content: '';
display: block;
background: black;
}

We set the position of our puzzle container to relative, so that we can then have piece containers with position absolute which can overlap.

Next, we set the initial size of our piece (100px), set overflow to hidden and then set resize to both.

After this, we make sure that the internal container grows 100% and that its position is relative, so that our after pseudo-element can be anchored to the bottom right corner.

Finally, we create our actual piece, a black block, using a pseudo element.

It’s moving!

To be honest, there might not be many uses for this apart from some cool codepens, but it’s always nice to see how much can be achieved without using JavaScript.

So… should we ditch JavaScript?

No, of course not. However it’s always good to consider when JavaScript is actually necessary.

We are not trying to cater here to the tiny fraction of web users who have JavaScript disabled. However, we need to remember that CSS is powerful, and provides us with many tools which we can use to build interactive components.

--

--