What’s Thrilling Today

Cross-Document State Management (An In-Depth Tutorial)

Screenshot of an art gallery grid of images with a filtering control.

This post is part of Intuitive+Learn, an exclusive series on That HTML Blog available with your Intuitive+ membership. Sign up here, or sign in to obtain your unlock key.

The timeline is healing. Web developers everywhere are pushing the boundaries of what’s possible with MPAs (Multi-Page Applications)—powered by concepts of hypermedia, islands, and static sites—and made better in terms of UX by technical advancements such as Cross-Document View Transitions.

One of the immediate challenges which will come up as you push the boundaries of MPAs is how to manage the state of interactive elements on a page as you navigate through different pages. For example, if you have an expandable section of content on a sidebar, and then navigate to another page, it can be disconcerting if the expandable section has suddenly collapsed again. Or if you filter a large body of content items by some particular criteria, and click on one of the items, how do you get back to that filtered result set easily?

In the past, one of the potential solutions was to store state in the URL itself as query parameters. But this can grow to be difficult quickly, especially as all disparate state on the page has to be munged into a single set of query parameters. Plus, for static sites, query parameters mean nothing…which means you’re writing JavaScript to interpret the URL-based state and manipulate the DOM accordingly. At that point, unless you obviously need URL-based deep linking to some particular state, why bother?

Here’s the thing: the browser provides a mechanism for saving and retrieving state. It’s called localStorage. You can save “anything” (well, small-ish strings essentially, but in most cases that’s quite enough) for later retrieval. However, the storage API is extremely simple. That’s both good and bad. It’s bad because it doesn’t give you much structure to build a framework around. The good news is: in terms of what you might think to build with it, the sky’s the limit!

This is what I decided I needed to build:

  1. An easy way to “namespace” different types of state within an application for better naming structure.
  2. An easy way to save JS objects with automatic conversion to JSON (typically via user events).
  3. An easy way to update state on a page immediately upon load based on what’s in storage (if any).
  4. A short-circuiting mechanism so when state does update the page, potential events don’t trigger like real user-based events.

After some thought, I came up with a name for this new API: retainedState. I wanted my new retainedState API to feel a lot like something which might be baked into the browser. It’s easy to use and has a minimal surface area, yet is surprisingly useful!

I’m not releasing this as an NPM library or anything, in part because it is so simple and in part because I think every website author should give these issues some dedicated thought. Perhaps you wouldn’t use my API as-is and make some tweaks first, or you are merely inspired by it and decide to write your own.

All I can say is, I’ve used a proto-version of this pattern on multiple production sites already, and now that I’ve arrived at more of standards-ish API for it, I’ll definitely be using this a lot going forward, and I hope it helps unlock some Aha! moments for you as well.

Let’s get started!

Included in this tutorial when unlocked:

  • A detailed explanation of how the retainedState API was built.
  • Three working demos:
    1. An “art gallery” with multiple artist and category pages.
    2. An image grid with persisted filtering.
    3. An HTML game with view transitions which remembers “room” state.
  • You’re encouraged to ask follow-up questions in the Human Web Collective Discord.

🙏 I’m excited to offer this new series in conjunction with Intuitive+, and I hope you enjoy it!



Designing an Autogenerated Live Color Scale in CSS

Promotional graphic for Willamette theme showing a deep blue river, a while sailboat, and a row of green trees along the riverbank.

When I started work on the Willamette theme for Bridgetown (a Ruby-based web framework), I knew I wanted to provide the user developing their new Bridgetown site the ability to choose a single “brand” color and then get an entire scale generated based on that color. All of the various components and layouts would then adopt the color scheme by default, simply based off of one --wll-brand-color CSS variable.

Until fairly recently, there was no way to do this live using vanilla CSS!

You could do something like it at build-time using SASS. (Here’s an example.)

Or you could do something like it “live” using JavaScript—but purely for demo/one-off purposes. (Presumably you’d have to copy the CSS code it provides and bring it into your project. Here’s an example of a tool which does that for you.)

However, now that we have real CSS functions calculating color math in real-time, we have the ability to guide the user in setting a single color in their stylesheet—and everything else will come along for the ride.

The Willamette theme provides a Style Guide with a live color picker to accomplish exactly that!


All of the magic happens in this set of variable declarations:

--wa-color-brand-95: color-mix(in display-p3, var(--wll-color-brand), white 90%);
--wa-color-brand-90: color-mix(in display-p3, var(--wll-color-brand), white 80%);
--wa-color-brand-80: color-mix(in display-p3, var(--wll-color-brand), white 65%);
--wa-color-brand-70: color-mix(in display-p3, var(--wll-color-brand), white 40%);
--wa-color-brand-60: color-mix(in display-p3, var(--wll-color-brand), white 23%);
--wa-color-brand-50: var(--wll-color-brand);
--wa-color-brand-40: color-mix(in display-p3, var(--wll-color-brand), black 19%);
--wa-color-brand-30: color-mix(in display-p3, var(--wll-color-brand), black 30%);
--wa-color-brand-20: color-mix(in display-p3, var(--wll-color-brand), black 45%);
--wa-color-brand-10: color-mix(in display-p3, var(--wll-color-brand), black 65%);
--wa-color-brand-05: color-mix(in display-p3, var(--wll-color-brand), black 80%);

Using the color-mix function, we can alter an existing color with a percentage of either white or black to serve as a lightening or darkening function.

All of these values were hand-chosen by me after careful evaluation, initially based on emulating the Web Awesome color scale of its blue hue. Web Awesome’s color palettes are manually designed, not autogenerated, so ultimately it’s superior (yes, humans still beat automation!). But I attempted to get as close to them as I could.

Some types of colors are more successful than others. The purple scale is quite lovely (try #9951db in the linked Style Guide). If you compare Web Awesome’s with Willamette’s autogenerated, there’s almost no difference. On the other hand, the yellow scale is much less impressive because Web Awesome rightfully skews into a bright yellow in the lighter tones, whereas Willamette sort of just goes beige. The darker tones are mostly comparable however.


Now a bit about color spaces.

Display P3 has been available for use in color functions since 2023, along with many other color spaces.

Why use Display P3? If we’re just mixing white for lightness or black for darkness, what’s wrong with SRGB? Or maybe we should use one of those new hotness spaces everyone talks about like OKLCH?

Well…here’s SRGB:

Screenshot of the Willamette Style Guide using the SRGB color space.

It’ll be a bit different for every color, but for our “Willamette green” brand color, the scale in srgb might not look too bad in the darker tones but it gets weird in the lighter tones. Far too saturated and almost turquoise, rather than an expected perception of a lighter forest green.

What about OKLCH?

Screenshot of the Willamette Style Guide using the OKLCH color space.

Bah! It gets way too gray and washed out in the lighter colors, whereas the darker colors are far too muddy and indistinct. OKLCH is incredible for many cross-hue gradients, as well as defining high-gamut colors using the oklch function, but in some cases it’s absolutely the wrong tool for the job.

Now, Display P3:

Screenshot of the Willamette Style Guide using the Display P3 color space.

With the display-p3 color space, the colors really do look like how you would expect—simply getting lighter or darker, and a bit desaturated of course. On a Display P3 monitor (which all my displays are, as I can’t stand SRGB-only), it’s the most obvious how nicely this works out.


Ultimately of course, there’s nothing stopping the user from manually setting their own colors in the scale within override CSS, perhaps for one or two colors if the autogenerated versions don’t look right. Or perhaps there could be a color-mix of the provided color-mix output with another color. COLOR MIX INCEPTION! 😱

If you’re interested in further exploration of CSS color functions, check out our previous coverage on That HTML Blog:

And remember, you can also use color-mix for adding an alpha value to any color (aka opacity)—instead of using white or black as the second color which controls lightness, use transparent to control the opacity!



The Best Introduction Guide to SVG You Will Read

Scalable Vector Graphics (SVG) is the image format for the web that isn’t raster (aka pixel-based) like JPEG and PNG. It’s vector-based as the name implies, meaning that the computer draws the image you see using a series of commands relating to paths, strokes, fills, and the like.

Something which sets SVG apart from other “image formats” however is that it’s more than an image format. It’s markup…which means it can be embedded in your HTML markup. This gives SVG superpowers, as Carmen Ansio so eloquently puts it in her fantastic article SVG from Scratch:

SVG clicked for me the day I stopped thinking of it as a file format and started thinking of it as a DOM. Every shape is an element. Every attribute is a style. You can select it with CSS, animate it with transitions, and target it with JavaScript, the same way you work with any other HTML element.

Most people learn SVG backwards: they export it from Figma, squint at the generated markup, clean up the IDs, and move on. That works, but you miss the part where SVG is actually fun to write by hand.

Have you edited SVGs by hand? Even made some significant changes directly in the markup? I have. I’m not sure I would describe the process as fun 😅…but it certainly was powerful and very handy. And Carmen is right on in thinking of SVG as a DOM, because manipulating it with CSS just like you would HTML (particularly with regard to animation) is the doorway to a whole new world.



Masonry Layout Solved! Hello CSS Grid Lanes

For two years now we’ve been covering the circuitous path wending its way towards getting a Pinterest-style layout mode into CSS (often called “masonry” layout). Our previous installments on the topic:

  1. The Future of Masonry Layout in CSS
  2. A Different Take on Masonry from the Chrome Team
  3. A Unified CSS Layout System: Could It Happen?

Well I’m pleased to report that our long web specs nightmare is finally over, and masonry layout has been solved! It’s no longer called masonry, nor is it part of some grand experimental unified CSS layout system. It is a new display type, but it otherwise inherits the layout system of CSS Grid, and it’s called Grid Lanes.

The syntax is blessedly straightforward:

.container {
  display: grid-lanes;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
}

And it’s available today in WebKit-based browsers such as Safari 26.4 as well as the latest GNOME Web. In fact, here’s a screenshot of a demo running in GNOME Web!

screenshot of grid lanes demo

The demo on CodePen is fun to play with and created by Alison Maher who is at Microsoft working with the CSS Working Group and the Blink engine to bring Grid Lanes to Chromium browsers. I’m not sure of the timeline for Gecko (Firefox), but word is they’re currently working on it as per this January 2026 article on the WebKit blog.

I think masonry/waterfall Grid Lanes layout is the coolest thing ever, so you can bet your bottom dollar that I’ll be implementing this everywhere once it reaches Baseline. 😁 OK, maybe not quite everywhere…but as close to it as I can get away with. You think I haven’t dreamed about updating my blog’s Photos page so it looks like a groovy gallery? Sure I could have reached for a userland JS solution like everyone else to date, but that just feels…janky. And multi-column galleries using a standard CSS grid are ugly, with all sorts of weird gaps because of the differences in aspect ratios.

Grid Lanes is the solution we’ve all been waiting for, and it’s very nearly here. Hip hip hooray! 🎉



Two Paradigms for Enhancing HTML Tags

Before I describe the two different paradigms for enhancing HTML tags, let’s first explain what it means to enhance an HTML tag.

I’ll use the <form> tag as an example, since I’ll be showcasing a couple of real-world form enhancements below.

So, let’s say you have a <form> element which has been instantiated as an object in your Document Object Model (DOM) as part of the HTML deserialization process (aka the markup is parsed, turned into DOM, and rendered to screen).

At the moment the form element is instantiated and connected to the document, you want to enhance the form by adding two features:

These enhancements are accomplished by executing JavaScript which sets up a reference to the form, adds event listeners, perhaps modifies the DOM to render some additional content, and exerts a degree of control over the lifecycle of the form.

The question then becomes, how do you set up this JavaScript? How does a wad of code get spun up in a 1:1 relationship with the element to be enhanced?

There is a bad way, and two good ways.

❌ First, the bad way:

<form id="some-form">
  ...
</form>

<script>
  const someForm = document.querySelector("#some-form")
  // do stuff with the form
</script>

(In the past, this would have likely been written using jQuery APIs, but the principle is the same.)

The reason this is bad is because you are brute-forcing the execution of the code in an imperative way, rather than using a declarative technique to execute the code whenever the relevant tag shows up. First of all, you likely want this code living in a tree of JS files somewhere else, rather than in a script tag next to the form. You might want the code to apply to various sorts of forms, not just one form with a particular ID. And why even use an ID at all (which is a fragile technique)? Plus this technique will completely break at any time you use reactivity or hypermedia: what if the form comes in via a fragment after the page has loaded? Will your code know that an ajax fragment has suddenly resulted in a DOM update?

The year is 2026…we have much better ways of accomplishing our goal!

Let’s look at the two good enhancement paradigms.

✅ I’ll pseudocode the first one, because there’s not yet a vanilla web API to make it work. Essentially, this paradigm is based on using a MutationObserver to look for a magic attribute on any element, and then inspecting that attribute in order to determine how to execute the relevant JS code to enhance the element. For example:

<form add-enhancements="validate autosave">
  ...
</form>

As soon as this form is instantiated, the observer will look for add-enhancements, parse the attribute into two terms: validate and autosave, and then based on a registry of available enhancements, run those two enhancements by passing along the form element so the enhancements can perform their tasks.

You may have previously used real-world solutions which operate on this principle, for example Hotwired Stimulus with its “controllers” concept. A web-native version of this would be the concept of custom attributes. They might work in a similar manner to custom elements, only instead of a 1:1 relationship between a custom HTML tag and JS element code running in a subclass of HTMLElement, there’d be a 1:n relationship: one tag could be enhanced by any number of bits of code via custom attribute lifecycles, and the tag itself would not be a custom subclass of HTMLElement but could be literally anything, even a standard form.

I think that would be a fantastic API, but I don’t believe there’s any immediate effort to implement it in browsers. Thus userland solutions will need to be used for these use cases.

✅ The second enhancement paradigm comes to us via that good ol’ web API we already know and love: web components.

Or perhaps more specifically, HTML Web Components as they’ve come to be called. These are custom elements which don’t really exist to serve as “components” in the design system UI sense of the word, but are there to enhance HTML which already exists.

Here are a couple of real-world form enhancements using this paradigm, which would be used in the markup like this:

<validation-enhancer>
  <form-saver>
    <form>
      ...
    </form>
  </form-saver>
</validation-enhancer>

This is a type of enhancement via element composition. By layering the form within one or more parent custom elements, you can modify the behavior of the form and add new features. This works in production today thanks to a couple of new libraries:

I think it’s fascinating that both of these projects launched just in the past few weeks, tackling totally different form problems but using the same paradigm of enhancement. While there are very specific cases where HTML web components aren’t a great solution (for example, you can’t insert custom elements inside of table markup, thus you’d have to wrap an entire table rather than any specific rows or columns), for the most part this is a tried-and-true way of authoring enhancements using vanilla web APIs.

I love seeing stuff like this! It would be awesome to see a lot more folks writing enhancements in this manner—at least until we do get a custom attributes API.


But…what about is=? Inevitably when this conversation comes up in discussions of web components’ capabilities, certain people will bring up the is= attribute and take a dump on Safari for nixing it. is= would let you write something like:

<form is="form-saver">
  ...
</form>

And then you could literally write a subclass of HTMLFormElement as a custom element which would get instantiated in this case.

Two major problems with this idea, and why I have always agreed with the WebKit team for nixing it:

In the above example, we might have used is="form-saver" to add autosave functionality to the form, but what if you also wanted the enhanced validation behavior? ❌ Too bad! 👎 You can’t add multiple terms to the is attribute because JavaScript doesn’t allow multiple inheritance. In other words, that form simply can’t be instantiated based on two or more class definitions. Just not how OOP works on the web or in JavaScript.

What we really need is the ability to enhance any existing HTML tag with an arbitrary number of enhancements. That’s why a proposed API like custom attributes, or today’s composition via multiple parent custom elements, are far preferable solutions than the is attribute and its limited direct inheritance.



The Web Platform is a Triumph of Object-Oriented Programming

In a past life, I wrote an article called HTML is a Serialized Object Graph and That Changes Everything which made the case why understanding the declarative nature of HTML as an object graph which sits alongside JavaScript’s imperative DOM API is so crucial to becoming a well-rounded web developer who prioritizes performance, accessibility, efficiency, and future-proofing.

I want to continue unpacking some of those concepts, namely efficiency, because every so often I come across a troubling perspective: that Object-Oriented Programming (OOP) is somehow “bad” and therefore the DOM is bad because it’s based on OOP.

Let’s be very clear here, and I consider this statement to be wholly undeniable:

The Web Platform Would Not Exist Without OOP.

It’s not just that web APIs often take advantage of OOP. They would not exist without OOP! Virtually every web API you have ever used is designed specifically within the OOP paradigm. And every entity in the lifecycle of a web page is based on objects. These objects are instances of classes and in a great many cases these classes are subclasses of other classes. This means that key functionality is spread across a class hierarchy. That isn’t a bug, it’s a feature! And this means that it’s absurd to think that to write a web app you need to reach for a “framework”. The Web Platform is a framework.

Here’s a list of many of Web Platform classes you have used (or the libraries you import use):

If you’re ever wondering what the superclass is (if any) for a particular class, you can write Object.getPrototypeOf(ClassNameHere) in the console. All of the native HTML elements types, such as HTMLDialogElement, will be subclasses of HTMLElement. This is why when you write your own custom element, you write class MyWebComponent extends HTMLElement. You are subclassing the same superclass as all the other elements in HTML!

You can also monkey patch classes and objects. This is quite common in many OOP paradigms, but generally frowned upon in the context of web/browser APIs because of the fear of future breakage. Nevertheless, if you know what you’re doing and choose a suitable naming convention, I say go for it! For example, there’s an example on MDN of how to get a blob URL from a fetched image resource which you can then pass along to an img tag. Let’s monkey patch Response to make this a little easier!

// Monkey patch
Response.prototype.userlandBlobURL = async function() {
    const blob = await this.blob()
    return URL.createObjectURL(blob)
}

// Test it out!
const resp = await fetch("https://placehold.co/600x400")
const blobURL = await resp.userlandBlobURL()
// 'blob:https://example.com/bc10ebbc-d1bc-44fa-a23e-3bf398061502'
const img = document.createElement("img")
img.src = blobURL
document.body.append(img)

Perhaps JavaScript should have some kind of “blessed” naming convention for “custom methods” the way HTML does for custom elements (<you-must-use-dashes>). At any rate, I chose userland as a prefix in this example. YMMV!

To go back to the word I used above, efficiency, the beauty of the web platform is once you learn these class hierarchies and the various relationships between the objects in the framework that is built into the browser, you are well on your way to understanding how to build in a high-quality fashion. As I’ve mentioned many times, the reason you don’t want to spray “div tag soup” all over your users’ download web pages is because every tag is an object instance. Got 5 <div>s? That’s five HTMLDivElement object instances loaded into memory. Got 5000 <div>s? That’s five thousand HTMLDivElement object instances loaded into memory. Thankfully browsers are themselves marvels of efficiency, written in low-level C++, so even then it might not be the end of the world. But don’t be an asshole and assume your tab is the only tab running on a machine.

Imagine a world where every damn browser tab has an app loaded in it with an incredibly over-engineered and overwrought DOM littered with needless element instances in a sprawling in-memory object graph. Sadly we don’t need to imagine it because that world is already here. But that doesn’t mean we need to be OK with it. We can push back and fight for a more considerate world, one where you have much more functionality written into far fewer objects because you understand what these objects are and how they work.

Don’t be caught flat-footed the next time a frontend engineer asks you: “So, what are all the objects loaded into memory as your web application boots up? Describe them to me.” Oh dear, has no one ever asked you that before?!

🚨 Web dev education still has a l-o-o-ong way to go… 🫠🤪



The Nuances of JavaScript Typing using JSDoc

As someone who has worked extensively on codebases using TypeScript, as well as codebases using JavaScript, I am here to inform you: I greatly prefer JavaScript.

It’s not that I don’t like specifying types for my variables and function signatures. I do! In fact, I like it so much I even do it in Ruby. 😲

But you see, what I don’t like is my types being part of “the code”. I believe code should be strictly about the behavior. What it’s called. What it does. The “metadata” surrounding the code—this is a string, that is an integer—makes perfect sense as part of documentation in the form of code comments. Because guess what? You should be documenting your code anyway. Whoever said don’t write lots of comments in code is grossly mistaken.

Yes indeed, it is my sincere belief that you should be documenting your functions, and your value objects, and your classes, and all sorts of other things as much as possible. (Without going overboard…usually a sentence or two is quite sufficient.) Which brings us to…

JSDoc.

When it comes to documenting JavaScript, JSDoc is the way to go. Even if you don’t expressly use the tool to generate an API site (I actually have never done that personally!), your JSDoc comments are interpreted by a wide variety of tools and editors. Which brings us to…

TypeScript.

Wait, if you prefer JavaScript, why are you talking about TypeScript??

Because TypeScript is how you type check JavaScript even if you’re writing JavaScript with JSDoc and not TypeScript. (Confused yet? 🥴)

Here’s an example. To declare a new string variable in TypeScript proper, you might write something like this:

let str: string

str = "Hello world"
str = 123 // this will cause a type error!

However, you can also get the exact same benefits in pure JavaScript by adding // @ts-check as the first line of a .js file and then specifying types via JSDoc:

// @ts-check

/** @type {string} */
let str

str = "Hello world"
str = 123 // this will cause a type error!

If you’re using an IDE like VSCode or Zed, you’ll likely see the type hints and errors show up automatically, but it also might be a good idea to npm install typescript -D because you may want to run tsc separately to generate TypeScript declaration files or to type check your files in a CI process. (In my package.json I have a script which looks like this: "build:types": "npx tsc".)

You can configure how the type checking works by adding a jsconfig.json file to your project’s root folder. Here’s one I use:

{
  "compilerOptions": {
    "strictNullChecks": false,
    "target": "es2022"
  }
}

At the same time, you’ll also probably need to add a tsconfig.json file at some point, like so:

{
  // Change this to match your project
  "include": ["src/**/*"],
  "compilerOptions": {
    // Tells TypeScript to read JS files, as
    // normally they are ignored as source files
    "allowJs": true,
    // Generate d.ts files
    "declaration": true,
    // This compiler run should
    // only output d.ts files
    "emitDeclarationOnly": true,
    // Types should go into this directory.
    // Removing this would place the .d.ts files
    // next to the .js files
    "outDir": "types",
    // go to js file when using IDE functions like
    // "Go to Definition" in VSCode
    "declarationMap": true
  }
}

I know that might all sound like a lot, but I promise you once you get a project going with your editor and your CLI tooling, you can replicate that setup across countless more projects and it becomes second nature.

Rather than continue to just talk about using JSDoc for typing, let’s dive into some examples!

JSDoc in the wild

Here’s a class constructor which accepts a number of arguments.

class ReciprocalProperty {
 /**
   *
   * @param {HTMLElement} element - element to connect
   * @param {string} name - property name
   * @param {(value: any) => any} signalFunction - function to call to create a signal
   * @param {() => any} effectFunction - function to call to establish an effect
   */
  constructor(element, name, signalFunction, effectFunction) {
    this.element = element
    this.name = name
    this.type = this.determineType()
    // etc.
  }
}

As you can see, it’s fine to keep using any at times when you really don’t “care” about the exact nature of the variable in question. And again, the nice thing about putting your type information in as part of the code documentation is…now you have documentation! 🙌

Here’s an example of specifying a variable type that is a JavaScript object (a “record” in TypeScript parlance) with typing for the key/value pairs:

/** @type {Record<string, ReciprocalProperty>} */
const attrProps = this.element["_reciprocalProperties"]

You can also see here we’re calling this.element["_reciprocalProperties"] instead of this.element._reciprocalProperties because TypeScript gets fussy about accessing properties it doesn’t know about. Using [] syntax bypasses those complaints (just make sure you know what you’re doing!).

Here’s an example of specifying a type inline as part of a for…of loop:

for (const stop of /** @type {StreetcarStatementElement[]} */ ([...this.children])) {
  stop.operate()
}

That particular syntax took me a while to figure out! 😅 When in doubt, put your inline /** @type */ declarations in front of a piece of code wrapped in parentheses. That usually does the trick.

Here’s an example of specifying a type inline within a method signature:

htmx.defineExtension("streetcar", {
  handleSwap: (swapStyle, _target, /** @type {Element} */ fragment) => {
    // etc.
  },
})

And here’s an example of importing just a type (aka not a standard JavaScript import) using @typedef syntax:

/**
 * @typedef { import("./HostEffects.js").default } HostEffects
 */

// later on…

/**
 * @param {HostEffects}
 */

You can also use @typedef to define the equivalent of TypeScript’s interface which is documented here.

And finally: every once in a while, you may just need to do something funky that TypeScript simply isn’t going to like, and that’s OK! As the documentation states:

TypeScript may offer you errors which you disagree with, in those cases you can ignore errors on specific lines by adding // @ts-ignore or // @ts-expect-error on the preceding line.

JavaScript + JSDoc + tsc should be the industry default

I have stated on multiple occasions that I believe it has been a huge mistake for “TypeScript” to become a sort of industry default. I firmly encourage everyone I talk to to start writing real web open standard .js files which don’t require any build steps or tooling to execute properly, all while utilizing the power combo of JSDoc + tsc to gain all of the benefits of type hints in IDEs and type checking in CI. It really is the best of both worlds, and the number of cases when you must give in and start authoring .ts files proper is vanishingly small.

It’s possible there are certain frameworks you may need to use which essentially “require” you to author your code using TypeScript. In those instances, so be it. But if you have any control over the shape of a project, I hope you’ll consider using good ol’ fashioned JavaScript. After all, it’s ECMAScript—not TypeScript—that is the lingua franca of the web.



Write Custom Extensions for htmx to Make it Your Own Personal Toolkit

Hot take: the killer feature of htmx which transforms it from a nice lil’ opinionated DSL for HTML-based reactivity to a powerhouse full-stack framework with which you can build ambitious, even experimental application architectures is this:

Extensions.

Unfortunately, if you follow the link above to “Building htmx extensions”, there’s virtually no documentation there. 😒

So I’m guessing the answer to Why didn’t anyone tell me about this sooner?? is likely Because nobody knows!

I’m here to rectify that situation; in particular, explain the ability for you to build your own custom swaps.

What is a Custom Swap? (for that matter, what is a swap?!)

In htmx in a nutshell, the following example is provided:

<button
  hx-post="/clicked"
  hx-target="#parent-div"
  hx-swap="outerHTML"
>
  Click Me!
</button>

In UX-speak, this translates to: “As a user, when I click the Click Me! button, I would like to see the new content appear in the box” (the “box” being a <div id="parent-div"> element somewhere on the page, and “content” being an HTML fragment returned by the server).

htmx comes with a number of swap mechanisms at your disposal, including ones like beforeend which is equivalent to targetEl.append(anotherEl) and even delete which ignores a response and simply deletes a target element.

Let’s call these built-in swaps. You can certainly accomplish a lot with only the built-in swaps, as evidenced by the growing popularity of htmx. However this leads us to the question at hand: Can I create my own swap?

Yes!

The genius of htmx is that the hx-swap attribute can be anything you want. hx-swap="bop"? Sure. hx-swap="snap-crackle-pop" Absolutely!

Yet the behavior you’ll end up with out-of-the-box is the same as innerHTML because you haven’t defined what these new swap mechanisms are. That’s where extensions come in.


Let’s write the most simple extension imaginable. Assuming there’s an htmx import or global defined, in your JavaScript entrypoint write:

htmx.defineExtension("bop", {
  isInlineSwap: (swapStyle) => swapStyle === "bop",
  handleSwap: (swapStyle, _target, /** @type {Element} */ fragment) => {
    if (swapStyle === "bop") {
      console.log("Bop!", fragment) // log the server HTML fragment
      return true
    }
  }
})

You’ll also need to add an hx-ext attribute on your site layout’s body:

<body hx-ext="bop">

Now you can update the button example from above!

<button hx-post="/whoop" hx-swap="bop">Boop!</button>

Clicking Boop! will go through the normal htmx ritual of calling the server and parsing the incoming HTML into a DOM fragment. However this time, your custom swap will kick in and you’ll get a Bop! console log. 🎉

Let’s talk about boosting for a moment. htmx provides an option where forms automatically submit using the AJAX technique so you can easily use swapping techniques. You can boost via the hx-boost attribute either on a container of your page or on <body> itself.

However, once you do this, your browser URL will change when htmx handles a request/response thus adding to your history. If you don’t want your custom swap to affect browser history and you don’t want to require users to fiddle with htmx’s history attribute, you can do so manually. Let’s add an event handler to our extension:

 onEvent: (name, event) => {
    if (name !== "htmx:beforeOnLoad") return

    /** @type {HTMLElement} */
    const elt = event.detail.elt
    if (elt.getAttribute("hx-swap") === "bop" && !elt.hasAttribute("hx-push-url")) {
      elt.setAttribute("hx-push-url", "false")
    }
  },

This sets the hx-push-url manually if it’s not already present on the element triggering the swap, thus indicating to htmx it shouldn’t mess with browser history.

And…that’s all folks! If you’ve ever looked at a library like htmx and wondered but what if I want to write my own code to deal with an incoming HTML fragment? (or am I the only weirdo who does this?! 😅), this is your solution.


DEMO TIME!

Have a Piece of Toast

It wouldn’t be a fresh That HTML Blog installment without a demo, so for this one I thought I’d come up with a fun little strategy of returning messages from the backend so the frontend can display a toast notification. Since my fav UI library Web Awesome sadly doesn’t come with toasts yet (unlike its Shoelace predecessor), I roped in the Simple Notify library which is actually pretty stellar.

In a nutshell, my custom toasts htmx extension pulls in HTML like this:

<output class="toast-notification" data-title="Success!">
  I'm a toast from the server!
</output>

And turns it into Notify-compatible JavaScript:

new Notify({
  status: toast.dataset.status ?? "success",
  title: toast.dataset.title,
  text: toast.textContent,
  effect: "slide",
  speed: 400,
  type: toast.dataset.type ?? "outline",
  position: "right bottom"
})

Could you have come up with your own vanilla JS solution, perhaps fetching some JSON instead and using that to populate the toast values? Sure…but then that wouldn’t be very htmx-y now would it?

The htmx ethos is all about using HTML as the source of truth in your application. HTML fragments can be used verbatim to populate the DOM, but why not use HTML an an interchange format as well? After all, once you introduce custom elements into the mix, you’re practically writing XML using a schema you invent for the task at hand. Is that a bad thing? I would say no! And considering the recent outcry over removing XSLT from browsers, I’m not the only one. 😂

So that’s htmx extensions. What will you create with it? Hop on over to Human Web Collective’s indie_web_dev@humansare.social Forum or Discord to chat about this and a whole lot more!



Add Terrific DX to Your UI Web Components with nimble-html

Previously on That HTML Blog, I wrote about a new library I released called Reciprocate. The TL;DR is that it lets you add signal-based reactivity (both properties & attributes) to your otherwise vanilla web components (particularly handy for “HTML Web Components” aka custom elements with templates already rendered by the server and preexisting in the DOM).

However, if you wanted to write more “traditional” frontend UI components with dynamic, declaratively-written templates defined in the custom element JS itself, you’d need to reach for something else. Fans of libraries such as Lit might wonder Where is my html function? I want my html function!

And that’s where nimble-html comes in!

Created by Joe Pea, it’s a 2.3kB minzipped library after my own heart because it does one thing and one thing only: give you an html function. 🙌 Essentially it lets you author tagged template literals containing HTML and extra syntactic sugar to set properties and add event handlers (just like in Lit!), and then insert those templates to any place in the DOM. It doesn’t even have to be anything related to a custom element, but obviously that’s where my brain goes.

In fact, as I read the details of how it works, it got me thinking: could I take some Reciprocate demo components and rewrite the HTML templates using nimble-html?

DEMO TIME!

Yes indeed, it works beautifully. With just a bit of boilerplate all tucked away in a BaseElement, you can write a new component with DX which feels very familiar:

class Counter extends BaseElement {
  static observedAttributes = ["count"]
  
  static {
    customElements.define("ui-counter", this)
  }

  constructor() {
    super()

    this.initialize(() => {
      this.count = 0
    })
  }

  get template() {
    return html`
      <style>
      output {
        font-size: 200%;
        vertical-align: middle;
        margin-inline: .5rem;
      }
      </style>
      <wa-button @click=${() => this.count--} variant="danger">- Dec</wa-button>
      <output>${this.count}</output>
      <wa-button @click=${() => this.count++} variant="success">+ Inc</wa-button>
    `
  }
}

Pretty awesome, right?! I love the idea that I could use Reciprocate alone to assist with some simple server-rendered HTML Web Components, and then if I needed more client-side smarts and lots of dynamic interactivity, I could simply import html from nimble-html and we’re off and away.

This really does prove out my thesis that the best way of thinking about web components isn’t that you want to adopt the One True Library to do it all. Rather, you should layer on small, self-contained APIs as needed and build up your own frontend stack that works for your specific projects and use cases.

Ideally in the future, more and more of those APIs are provided by the browser itself. In the meantime, we can reach for userland solutions which focus on doing one or two things very well.



Give “fetch” a Bit More Oomph with “ffetch”

I cannot sing the praises of fetch loudly enough. As a frontend (and backend!) JavaScript developer, having ready access to this native web API where you can pull down HTML fragments or JSON data with a line or two of code is simply brilliant.

But Gabor Koos wants you to know there are a few things lacking in such a simplistic approach when it comes to shipping robust production-ready code:

The network is unpredictable and you have to be ready for it. You better have answers to these questions: What happens when the network is slow or unreliable? What happens when the backend is down or returns an error? If you consume external API’s, what happens when you hit the rate limit and get blocked? How do you handle these scenarios gracefully and provide a good user experience?

Honestly, vanilla Fetch is not enough to handle these scenarios. You need to add a lot of boilerplate code to handle errors, retries, timeouts, caching, etc. This can quickly become messy and hard to maintain.

And the solution Gabor provides is a tiny 2.3KB wrapper around fetch called, appropriately, ffetch. Here are some of its features:

Here’s what a sample request looks like:

import createClient from '@gkoos/ffetch'

// Create a client with timeout and retries
const api = createClient({
  timeout: 5000,
  retries: 3,
  retryDelay: ({ attempt }) => 2 ** attempt * 100 + Math.random() * 100,
})

// Make requests
const response = await api('https://api.example.com/users')
const data = await response.json()

There are many more usage examples here, including how to write your own API client class with a lot of built-in smarts. In this regard, ffetch reminds me a bit of a Ruby gem I like to use when writing requests in a Ruby backend, faraday.

Some of you may be wondering “well, if I’m going to use a library instead of vanilla fetch, why not just use axios?”

Well, a couple of reasons I can think of. One is that ffetch mirrors the fetch API so you can use it as a “drop-in” replacement. Axios doesn’t even use fetch, it wraps the older XMLHTTPRequest API. Second, it comes in at a whopping 14KB minzipped—that’s 600% larger than ffetch. 🤯

So yeah. My advice is always to use native web APIs until you outgrow them (and get sick of writing a ton of boilerplate), then see if you can reach for something which is as lightweight and “vanilla-adjacent” as possible to layer on a few more smarts. ffetch appears to be a worthy candidate, and I look forward to trying it out on my next project.

Older Posts
Skip to content