While I target NuxtJS for this writeup, the base principles here can be (and are) used in a multitude of frameworks out there. It's not some new invention of mine, but as I hadn't seen it done within Vue or Nuxt, I found a way to do it myself. The general idea here is to have a class on the site's <html>
-tag (I use .no-js
) to help you style your way out of hidden content when there's no JavaScript available in the client.
Note: I am working based on this website, and thus I base this on the website being server side rendered and then hydrated in the client.
Starting with a real world case
Lets take a pretty solid example. Refresh this page.
Done? Good.
Did you notice how the content of the page faded in as the page loaded? That basic effect is achieved with something along the lines of this CSS:
.l-default__page {
transition: opacity 2s;
}
html[data-n-head-ssr]:not(.no-js) .l-default__page {
opacity: 0;
}
By looking at the data-n-head-ssr
attribute (an attribute set by the NuxtJS framework), I can keep the content of the page invisible until the HTML has been hydrated (which removes the attribute).
However, if the page is run without JavaScript (here's how to disable JavaScript in Chrome for testing), the data-n-head-ssr
attribute will never be removed, and the page content will be hidden away forever. Shit.
But... If you actually disable JavaScript on my website, you may notice that the fade will not happen anymore - and if you inspect the html of the website a keen eye may also notice a class on the <html>
-tag, that isn't there when JavaScript is enabled: The .no-js
class. And as you see in the above CSS, the opacity: 0;
rule is set to never happen if this class is carried by the <html>
-tag. Neat.
How to do it?
First and foremost, we need to make sure our HTML starts out with the class present on the tag. We do this by using the head
property in our nuxt.config.js
file (making it if it doesn't already exist). This property allows us to use vue-meta to let us, for example, setup parts of our website's <head>
, or (like we want right now) set attributes of our <html>
. Like this:
// nuxt.config.js
export default {
//...
head: {
htmlAttrs: {
class: 'no-js',
},
},
//...
};
Easy-peasy so far. Next, we will go ahead and remove it again, by (still within head()
) adding a script to the <head>
:
// nuxt.config.js
export default {
//...
head: {
htmlAttrs: {
class: 'no-js',
},
script: [
{
hid: 'no-js',
type: 'text/javascript',
innerHTML: `
document.documentElement.classList.remove('no-js');
`,
},
],
__dangerouslyDisableSanitizers: ['script'],
},
//...
};
A couple of things notable here. First, why make a script to remove it again? Well, because for this to work, we have to work subtractively. If JavaScript is disabled in the client, then we cannot add a class to the <html>
-tag through JavaScript. However, by doing it like in the above code block, the inserted script will only run when JavaScript is enabled, thus only remove the .no-js
class when JavaScript is enabled, and let it be if not. Makes sense?
The next things worth noting are of a more practical matter: hid: 'no-js'
is set on the script to tell vue-meta not to insert multiple copies of the same script (ie. we brand it with an unique id). Without this, a new script will be inserted every time vue-meta updates (for example on page changes). And secondly, to be allowed to use the innerHTML
property without Vue sanitizing the string (thus making it non-executable), we have to disable the sanitizers through __dangerouslyDisableSanitizers: ['script']
.
Now it all functions, kinda. With the above code, if you enter the website with JavaScript disabled, everything works as expected. And if you enter the site with JavaScript enabled, everything also works. Until vue-meta updates (on page change, hash/query update, or something else). Because when this happens, vue-meta also add the .no-js
class once more! (because that's part of what we have told it to do). So now suddenly the CSS behaves like when JavaScript is disabled, which is not very good.
We could possibly remove the hid
property from the script and allow vue-meta to insert duplicate scripts. But a bit more elegantly we do this:
// nuxt.config.js
export default {
//...
head: {
htmlAttrs: {
class: 'no-js',
},
script: [
{
hid: 'no-js',
type: 'text/javascript',
innerHTML: `
document.documentElement.classList.remove('no-js');
`,
},
],
__dangerouslyDisableSanitizers: ['script'],
changed() {
if (typeof window !== 'undefined') {
document.documentElement.classList.remove('no-js');
}
},
},
//...
};
changed()
simply allows us to hook into when vue-meta updates/changes, and then it's just a matter of removing .no-js
once more. And that concludes the setup - now your CSS can refer to .no-js
to adjust itself accordingly to the lack of JavaScript.
But is this really necessary?
Yes and no. It can be hard to think of a web without JavaScript - and in many cases this probably doesn't affect a thing. But some people purposefully disable JavaScript (for various reasons: distrust to what code it runs, low bandwidth and data savings, living life on hard mode...), and some bots (SEO crawlers and the like) cannot handle it. So while the reasons dwindle each and every day, there's still reasons out there to do it - if not just to be a little bit more inclusive.
Conclusions and caveats
This method of adding a class to the <html>
-tag has been done in various frameworks and tools before, most notably (maybe) in Modernizr. However, one caveat of this approach is, that it only helps if you can CSS your way to a solution. An accordion where the contents are hidden with display: none
, or a swiper where the items are simply out of view? Sure, no problem - overwrite the CSS by showing the hidden content and stack the swiper items vertically instead. But if said accordion or swiper is changing the DOM dynamically through JavaScript... Well, then you're out of luck ¯\_(ツ)_/¯
And sometimes that's just the case. Most of the websites I work on, has some functionality or another, that heavily relies on JavaScript, which you cannot just replace with CSS. And that's okay. But I hope this can help somebody out there.