In the past weeks, I put a lot of effort into making this website work without JavaScript as much as possible.
Don't get me wrong - I love JavaScript, and use it for many fun features across my site. However, I also want it to work for people with JavaScript disabled. Fortunately, modern CSS - or even just HTML - can replace many use cases where scripting would have been needed just a few years ago.
Using Eleventy as a static site generator also gives me some opportunities to move logic from client to the server, to be executed during build time.
This post assumes you know HTML, CSS and JavaScript. Some parts also talk about Eleventy, but you'll probably understand without knowing it.
Hamburger menu #
The (mobile only) hamburger menu has been a hot pile of mess ever since I added it.
Yes, this is the actual code powering the hamburger menu before the rewrite 🥶
Here's how I replaced the logic with pure CSS:
<header>
<label for="burger-menu-checkbox">
<!-- menu icon -->
</label>
</header>
<input type="checkbox" id="burger-menu-checkbox">
<div class="menu">
<!-- menu content -->
</div> Toggling the menu is done by (un)checking the checkbox. Notice how the menu icon is not the checkbox itself, but a <label> for it. It's a standard HTML feature that clicking on a label toggles its checkbox.
Now to the CSS:
/* hide the checkbox and menu */
#burger-menu-checkbox, .menu {
display: none;
}
/* show the checkbox if it's checked,
put it across the whole screen
and make it invisible */
#burger-menu-checkbox:checked {
display: block;
position: fixed;
top: 0;
width: 100vw;
height: 100vh;
z-index: 1;
appearance: none; /* don't show it (invisible) */
backdrop-filter: blur(1px) brightness(90%);
}
/* show the menu if it's next to a checked checkbox */
#burger-menu-checkbox:checked + .menu {
display: block;
position: fixed;
top: 4rem;
width: 100%;
z-index: 2; /* the menu is above the checkbox */
}
header {
z-index: 3; /* header is above both checkbox and menu */
} This is a well known technique called the checkbox hack, but with a twist I believe I might be the first person to come up with:
Because I used the <label> as a menu icon, I have a spare hidden checkbox that stops being hidden once it is checked. It covers the whole screen, but is under the menu, which means that anywhere you click outside of the menu, you click on the checkbox - and uncheck it.
appearance: none basically means the checkbox is invisible, but the actual element is present and can be styled with something like background-color, or - in my case - backdrop-filter to blur and dim everything behind it. And, of course, you can click on it.
The menu can now be dismissed by clicking outside, just like the user expects. All without JavaScript!
The only thing I eventually had to use JavaScript for was closing the menu on clicking on a menu item, because not all of them lead to a different page and so don't close the menu as a side effect.
That also fixes a minor issue that browsers remember if checkbox was checked when you leave the page, and keep it checked if you go back in history. It remains to be an issue with JavaScript disabled, but it's just a small annoyance IMO.
Language filtering #
On pages with lists of posts (/blog and tag pages), there's a drop-down select to only show posts written in a certain language.
Using the Liquid templating languge, which is the default in Eleventy, I'm generating the list of posts. Each posts is a <li> with a class indicating its language - that is language-czech or language-english.
{%- for post in collections.blogpost reversed -%}
<li class="language-{{ post.data.language }}">
...
</li>
{%- endfor -%} This is how the rendered HTML of the page looks like:
<select id="languageFilter">
<option selected value="all">No filter</option>
<option value="english">English posts only</option>
<option value="czech">Czech posts only</option>
</select>
<ol class="post-list">
<li class="language-english"> ... </li>
<li class="language-english"> ... </li>
<li class="language-czech"> ... </li>
</ol> Using just two CSS selectors (one for each language), I implemented the filter without any JavaScript:
#languageFilter:has(option[value="czech"]:checked) + .post-list li:not(.language-czech) {
display: none;
}
#languageFilter:has(option[value="english"]:checked) + .post-list li:not(.language-english) {
display: none;
}
In human language, this is what the first one means:
- Find element with id
languageFilterthat contains an<option>element with valueczechand which is selected. - Find element with class
post-listthat is right after it. - Find all
<li>elements inside which do not have a classlanguage-czech. - Hide them.
In simpler words - If the filter is czech, hide all posts without the language-czech class.
The second selector is the same but for English. It would be fairly simple to generate these rules automatically for any number of languages - I even began with the implementation, but then I realized I'm not going to learn a new language any time soon, so I might as well keep it simple.
Music status #
I have a widget on the home page that displays which song I am currently listening to. It pulls the song info from ListenBrainz, and also fetches cover art.
There's no way I could do it without JavaScript. Luckily, someone named Catou made a free API for ListenBrainz that that returns an image. I can just put the image on my website and every time a user loads the image, it will have the current song.
It's appearance is not very customizable, and neither is the functionality. And it's not accessible. But it doesn't require JavaScript, because it's just a regular <img> for what the browser is concerned.
Fortunately, I don't have to choose between the two options. I can put the image version inside a <noscript> tag, which is rendered only in browsers without JavaScript.
<noscript>
<img src="https://aoi.tana.moe/stepanz?color_mode=light" width="100%" alt="music status from listenbrainz. enable javascript for accessible version." />
</noscript> This is how it looks like:
Now I just need to hide the (nonfunctional) JS version. Hiding elements in browsers without JavaScript is also going to be useful later, and the method I use comes from Manuel Matuzović's article:
<html lang="en" class="no-js">
<head>
<script>
document.documentElement.classList.replace('no-js', 'js');
</script> The <html> element has a no-js class, but that class is replaced with js. If JavaScript is disabled, the replacing code doesn't run and so no-js is kept. Clever.
I can now create an utility class that hides an element if JS is disabled:
html.no-js ._hide-on-nojs {
display: none !important;
} All I had to do at this point was to give the regular music status a _hide-on-nojs class, and I was done.
Pop-ups #
This one is easy, because all modern browsers now support the <dialog> element:
The
<dialog>HTML element represents a modal or non-modal dialog box or other interactive component, such as a dismissible alert, inspector, or subwindow. (MDN)
My code for the RSS button in the footer is just this, and works without any scripting.
<button command="show-modal" commandfor="rss-dialog">RSS</button>
<dialog id="rss-dialog" closedby="any">
<h2>Feeds for my blog</h2>
<ul>
<li><a href="/feed.xml">English posts</a></li>
<li><a href="/all.xml">All posts</a></li>
<li><a href="/czech.xml">Czech posts</a></li>
</ul>
</dialog> Back to top button is easy #
(Title of this section is weird because reasons)
Now, this is pretty simple. The JavaScript way of doing this is
<button onclick="document.documentElement.scrollTop=0">Back to top</button> Without JS, you can use a link that points to any element on the page using its id.
Every page on my website has a <main id="main-content"> element, so I can do this:
<a href="#main-content">Back to top</a> And the only disadvantage is that the url now has #main-content at the end.
That's a fair trade off, and I can combine those two methods so it happens only for visitors with JavaScript disabled:
<a id="scroll-to-top" href="#main-content">Back to top</a>
<script>
document.getElementById("scroll-to-top").addEventListener("click", e => {
e.preventDefault()
document.body.scrollTop = 0;
document.documentElement.scrollTop = 0;
})
</script>
(The document.body line does nothing in my browser, but I've seen it somewhere and added it just in case.)
Syntax highlighting #
Syntax highlighting means taking code blocks like you see in this article and making them colorful based on the code. That's no easy task, but we have libraries for that.
I'm including it here because of the number of website that do this in your browser, with JavaScript, every time you load the website.
On my site, it happens once at build time, on my computer - just as god intended.
I use Shiki's markdown-it integration, and it highlights using the same engine VSCode uses. There's an official highlight plugin for eleventy that uses PrismJS, but it supports less languages and highlights poorly.
The rest #
Besides from what I already described, I've hidden a few buttons using the _hide-on-nojs utility class here and there, and added some red alerts wrapped in <noscript> on a few pages that really cannot work without JavaScript.
One thing that remains is the comment form. It's a simple html <form>, so it could work without JS, but there's currently some additional logic in use which couldn't.
I'll probably have to setup my own little server to receive the comments. But for now, there's just warning that sending comments doesn't work.
Viewing comments works fine however, as they are a static part of the website rendered at build. You can read more about it in the Colophon.
Test your website #
That's all I've got...
If you want to test how your website works without JavaScript, it's pretty straightforward.
There are many ways to disable scripts, but the easiest one is using uBlock Origin (if you don't use it, you should!)
Just click on the uBlock extension and then on the rightmost icon, as shown in the following image:
Now just reload the site and see if it works :)
Thanks for reading. Any comment will be greatly appreciated ❤️