The tiny trick of the day: html[data-local-storage]
Tags: html, javascript, webdev, css
Published: 2022-06-08 09:41:12

There are times when you want to create settings that only need to be stored locally in a specific browser. For these use cases the localStorage is very much the ideal place.

At times you wish to have these settings effective immediately upon a page load. Is there a nice way to do it?

## Handling the state on page load

Say you want to create a feature that only loads a specific font if a setting for that is in use, but not bother with it otherwise. This means that you need to link up JavaScript with CSS loading the fonts + activate the style to make use of the loaded fonts.

For this use case I have a little trick: expose localStorage keys via HTML document level dataset!

```js
// code in <head />
<script type="module">
function update() {
	document.documentElement.dataset.localStorage = Object.keys(localStorage).join(' ');
}

window.onstorage = function () {
	requestAnimationFrame(update);
}

update();

if (localStorage.loadFont != null) {
	const link = document.createElement('link');
	link.rel = 'stylesheet';
	link.href = '/fonts/stylesheet.css';
	document.head.appendChild(link);
}
</script>
```

Now if there is a key `loadFont` then a CSS file is loaded. Also, if any other browser tab changes the localStorage key it is immediately reflected on the current page as well.

## Styles

Setting up styles is easy enough:

```css
html[data-local-storage~='loadFont'] {
	/* Use whichever font-family loaded by the stylesheet */
	font-family: my-custom-webfont, sans-serif;
}
```

Now font changes immediately as the key is in the dataset, and in localStorage.

## Triggering it

Of course you still need to also have something to enable and disable the key in localStorage. At simplest this could be:

```html
<button type="button" onclick="localStorage.loadFont='';location.reload()">Load font</button>

<button type="button" onclick="localStorage.removeItem('loadFont');location.reload()">Remove font</button>
```

This code does not have the logic to inject the font stylesheet and to update the dataset. Instead page reload is used to trigger the desired end result.

## Making it robust

There is one consideration that makes the code fail: in some conditions localStorage is not really usable. For example Safari's private browsing lets you see localStorage, but actually using it will fail.

So let's update the code a bit:

```js
// code in <head />
<script type="module">
try {
	localStorage.removeItem(localStorage._ = '_');

	function update() {
		document.documentElement.dataset.localStorage = Object.keys(localStorage).join(' ');
	}

	window.onstorage = function () {
		requestAnimationFrame(update);
	}

	update();

	if (localStorage.loadFont != null) {
		const link = document.createElement('link');
		link.rel = 'stylesheet';
		link.href = '/fonts/stylesheet.css';
		document.head.appendChild(link);
	}
} catch (e) {}
</script>
```

This effectively adds a new feature: if `html[data-local-storage]` attribute does not exists then we can't use localStorage!

This means we can now also have conditional rendering, so we can expand CSS with a new class:

```css
html:not([data-local-storage]) .depends-on-local-storage {
	display: none;
}
```

Now you can have `<div class="depends-on-local-storage">...settings...</div>` which is only visible to the user if they can actually make use of the options!

You can also use a simple client-side JS to check for this safely:

```js
const hasLocalStorage = document.documentElement.dataset.localStorage != null;
```

Attempting to directly use localStorage would throw, or might only throw upon trying to use it, so this is a safer way to do the check. Also that explains the earlier "weird code" with removeItem.

### Checking if the stylesheet file was actually injected

When implementing a more suitable client-side JS toggle using your favorite front-end lib (which should be SolidJS or Preact) you might want to check if the font stylesheet was already loaded.

My suggestion for this:

```js
// reminder: needs to be in the toggle to keep the dataset updated
document.documentElement.dataset.localStorage = Object.keys(localStorage).join(' ');

// here `loadFont` is a boolean which knows if the setting is on/off
if (loadFont && !document.querySelector('link[href$="/stylesheet.css"]')) {
	const link = document.createElement('link');
	link.rel = 'stylesheet';
	link.href = '/fonts/stylesheet.css';
	document.head.appendChild(link);
}
```

## Have fun!

So we implemented a tiny boolean based feature that allows you to globally:

1. Know if localStorage is available
2. Enable a setting immediately upon page load
3. Toggle the setting

The benefit of this implementation is that the JS is small enough that it can be within each HTML page, thus no need to wait for the client bundles to be loaded and kick in. This eliminates any possible chance of undesired flicker (not counting the delay of webfont loading).

The downside of this specific implementation is that it is limited to boolean values only. This is a sacrifice made to have simple code. In some cases it might also be undesirable for all keys to be added to the dataset. However adding a whitelist feature should be easy enough.

In case wondering I'm using this to provide a toggle to load [OpenDyslexic](https://opendyslexic.org/).