No JavaScript needed

Table of contents

I have never been a fan of JavaScript. I don’t want it on my personal website. And I found ways to do things I usually would need JS for with static HTML and CSS.

After a phishing attack on my friend’s computer I started to look closer at all the code I run without ever reading it. That attack was Node.js, not JavaScript in a browser - but its core trick, fetching code from a remote server and executing it on the spot, is exactly what every third-party <script> tag on a website does. So I decided to ditch JavaScript entirely, where possible. I started to use NoScript and saw how awful most of the websites on the Internet are, if you disable JavaScript. Tons of external integrations (tracker?, analysis of user behaviour?) and completely broken sites where even the first load of content is not executed.

I did not want to put users on my page in the same situation so I removed almost all JavaScript functionality from my site here and replaced it with static HTML and some CSS. Let’s have a little walk through what I normally would use JavaScript for and how I replaced it.

Why JavaScript is used

JavaScript is often used to add features that HTML is not capable of. But then people forget to remove JavaScript when HTML finally added support for a certain feature, f. ex. lazy loading of images. Browsers have done this natively for years, yet many sites still ship a library for it. Every script that stays is code the visitor has to download, parse and execute - on every single page load.

Another thing is tracking. Analytics, session recordings, heatmaps - all of this runs as JavaScript in the visitor’s browser. It collects data the visitor never agreed to share and it is often a reason why a site feels slow.

Or third-party integrations. Embedded videos, social media buttons, chat widgets or comment sections load scripts from other servers. Each of them tells another company that someone visited my page, which is something I do not want.

So either I can build it into my site directly, without relying on third-party JS integrations, controlling what goes in and out, or I skip it.

What I replaced

I understand, there is probably not a real HTML-only (in combination with CSS) alternative to every functionality you want to have on your website. I found that for my use cases I do not need to rely on JavaScript - and hopefully will not in the future.

Contact Form

Usually you would send your contact form with JavaScript. No page reload, nice success message once sent, or showing some errors underneath the input fields. My contact form uses staticform, my custom written form backend for static pages.

I get HTML validation of fields on the frontend using HTML5 attributes1 and proper field types.

1<input id="email" name="email-address" type="email" required />

After submitting, the backend service validates again and redirects to a success or error page.

No JS needed.

A search on a static site usually means shipping a search index as a big JSON file and some JavaScript that crawls through it. Search engines have already indexed my page, so why not use them?

My search page is a simple form that sends the query to DuckDuckGo and limits the results to my domain with a hidden field.

1<form action="https://duckduckgo.com" method="POST">
2    <input type="text" name="q" placeholder="Search this site..." required>
3    <input type="hidden" name="sites" value="www.codedge.de">
4    <button type="submit">Search</button>
5</form>

I chose DuckDuckGo because it does not track searches, save IP addresses, or build user profiles.

No JS needed.

Mobile menu

Using details element

The classic hamburger menu on mobile does not need JavaScript. HTML has an element for exactly this use case built-in: <details>. The <summary> is the clickable part, everything else is shown when it is open.

 1<details class="group md:hidden">
 2  <summary class="list-none [&::-webkit-details-marker]:hidden" aria-label="menu">
 3    <span class="group-open:hidden">[OPEN ICON]</span>
 4    <span class="hidden group-open:inline">[CLOSE ICON]</span>
 5  </summary>
 6  <div class="flex flex-col gap-2 pt-6">
 7    <a href="/menu-item-1">Menu item 1</a>
 8    <a href="/menu-item-2">Menu item 2</a>
 9    <a href="/menu-item-3">Menu item 3</a>
10  </div>
11</details>
layouts/partials/menu.html

The browser keeps track of the open state and sets the open attribute on <details>. With Tailwind’s group-open: variant I swap the menu icon for the close icon. list-none and the ::-webkit-details-marker selector remove the default triangle. On larger screens md:hidden hides it completely and the normal navigation shows up.

Using Popover API

A newer option is the Popover API, available in all major browsers. The popover attribute turns any element into a popover, and a button with popovertarget toggles it.

1<button popovertarget="menu" aria-label="menu">[OPEN ICON]</button>
2<nav id="menu" popover>
3  <a href="/menu-item-1">Menu item 1</a>
4  <a href="/menu-item-2">Menu item 2</a>
5  <a href="/menu-item-3">Menu item 3</a>
6</nav>

It works a bit differently than the details element with regard to placement and closing with the Esc key. Still, a very good alternative to JavaScript.

No JS needed.

Image Lazy Loading

For a long time lazy loading images meant a library watching the scroll position and swapping a data-src into the src attribute. Browsers have supported this natively for some years.

1<img class="img" alt="..." src="/posts/00020/cover.jpg"
2     loading="lazy" decoding="async"
3     height="1080" width="1920">
  • loading="lazy" loads the image only when it gets close to the viewport
  • decoding="async" does not block rendering of the remaining page while decoding the image
  • height and width let the browser reserve the space, so the layout does not jump around

No JS needed.

Reading progress

A bar on top of the screen that fills while you scroll through a post. This is usually an event listener on scroll calculating the percentage and setting the width of an element. With scroll-driven animations CSS can do this on its own.

 1@keyframes reading-progress {
 2    to { background-size: 100% 100%; }
 3}
 4
 5@media (prefers-reduced-motion: no-preference) {
 6    @supports (animation-timeline: scroll()) {
 7        body:has(main > div > article)::before {
 8            content: "";
 9            position: fixed;
10            inset: 0 0 auto 0;
11            z-index: 10;
12            height: 5px;
13            background: linear-gradient(var(--color-accent), var(--color-accent)) no-repeat left / 0% 100% var(--color-zinc-200);
14            animation: reading-progress 1ms linear both;
15            animation-timeline: scroll(root);
16        }
17    }
18}

Scroll-driven animations are not supported in every browser yet, currently Firefox is lacking support for this. Honestly this is not a big problem for me: browsers without support simply do not show the bar, and Chromium-based browsers still cover a big chunk of the browsers people use.

No JS needed.

Closing thoughts

With all these changes I am quite happy with the balance of interactivity on my site and features for reading content - and all this without JavaScript.

There is one tiny script left: a visitor counter using GoatCounter, a privacy-respecting analytics solution. It does not use cookies or track individual visitors. It follows my understanding: I host the script on my own server, so no third-party code runs on my page - only the page view itself is sent to GoatCounter. You can find the link to the dashboard in the footer section.

(Almost) no JS needed.

This content was created on and updated on .
Enjoying codedge?

If I helped you in some way with my writing, a small contribution would support me. Your support keeps me creating!
Send a tip