_hyperscript is a scripting language for adding interactivity to the front-end. You guessed it, yet another JavaScript library. But what makes hyperscript unique is that it provides a natural language syntax for optimal readability, reusability and maintainability, arguably the most important qualities of any “programming” language.
Let’s begin by taking a look at some hyperscript.
<button _="on click add .hidden">
...
</button>
The underscore (_
) attribute is where you put hyperscript by default, although you can also use a script
or data-script
attribute, or even create your own custom attribute name. The hyperscript above can be translated as follows:
When the button element is clicked, add the class
hidden
to it.
Readability #
You’ll notice that hyperscript reads like terse documentation, and this is one of its core principles: define behaviour in natural language terms in the locality of the element that it relates to.
Let’s extend the example above to a common use-case, a button that opens/closes a menu when clicked.
<button _="on click toggle .hidden on #menu">
...
</button>
<div id="menu">
...
</div>
It is not only immediately obvious what the hyperscript above does, but you may already begin grasping how to use and extend this pattern for other use-cases.
Rather than explain what the next example does, I’ll let you figure it out yourself.
<input name="title" type="text" value=""
_="on change show #warning when my value is empty"
>
<div id="warning">
The title field is required.
</div>
Locality of Behaviour #
Defining behaviour in the locality that it relates to makes it easier to find and reduces cognitive load. Let’s look at another example.
<select id="weather" name="weather"
_="on change show #specify when my value is 'other'"
>
<option value="sunny">Sunny</option>
<option value="cloudy">Cloudy</option>
<option value="other">Other</option>
</select>
<div id="specify">
Please specify...
<input name="other" type="text" value="">
</div>
We just created a conditional field with a single line of hyperscript. The equivalent functionality written in jQuery and vanilla JS might look something like this.
//--- jQuery ---
$('#weather').on('change', function() {
if (this.value == 'other') {
$('#specify').show();
}
else {
$('#specify').hide();
}
});
//--- Vanilla JS ---
document.getElementById('weather').addEventListener('change', function() {
if (this.value == 'other') {
document.getElementById('specify').style.display = 'none';
}
else {
document.getElementById('specify').style.display = '';
}
});
That’s quite a bit of code to show or hide a field based on another field’s value. More importantly, that JavaScript code will likely be placed either at the end of the document, or in a script.js
file somewhere else in the web directory. So as a developer seeing this markup for the first time (or after a long time), you’re not going to have any idea what behaviours (event listeners) are attached to the select field. The only way to figure this out is to search for the relevant code and then hope that it makes sense to you.
Scripting, not Programming #
Both the jQuery and vanilla JS approaches above are solved programmatically. This may be second nature for experienced programmers, but there is a translation step required. The original feature request is as follows:
Any time the “weather” is changed, show the “specify” element if the selected value is “other”.
In the hyperscript approach, we were able to write this as simply as:
<select id="weather" name="weather"
_="on change show #specify when my value is 'other'"
>
In the vanilla JS approach, we had to translate it to:
Find the “weather” element in the document, add an event listener to it that listens for a change event and when that event is triggered then check if the value is “other”, if it is then find the “specify” element in the document and set its style display property to “none” otherwise find the “specify” element in the document and set its style display property to a blank string.
The jQuery approach is admittedly easier to read and write, but the same programmatic mindset is required when both writing and reading the code.
Even a comparable library like Alpine.js, which strives for locality of behaviour, requires a programmatic approach.
<div x-data="{ open: false }">
<select id="weather" name="weather"
x-on:change="open = ($el.value == 'other')"
>
<option value="sunny">Sunny</option>
<option value="cloudy">Cloudy</option>
<option value="other">Other</option>
</select>
<div id="specify" x-show="open">
Please specify...
<input name="other" type="text" value="">
</div>
</div>
In the Alpine.js approach, we first need to wrap our markup in a div
with an x-data
attribute to mark it as an Alpine component, in which we define the variable open
with an initial state of false
. We bind the variable’s value to the “specify” element using the x-show
attribute. And finally, we add a change event listener to the “weather” element that sets open
to true
if its value is “other”, otherwise to false
.
So even the Alpine.js approach takes significant cognitive load, both to write and to understand, and requires a programmatic mindset.
Let’s compare this to a variation of the last hyperscript example we saw and then discuss what we’ve done below.
<select id="weather" name="weather">
<option value="sunny">Sunny</option>
<option value="cloudy">Cloudy</option>
<option value="other">Other</option>
</select>
<div id="specify"
_="on change from #weather show me when its value is 'other'"
>
Please specify...
<input name="other" type="text" value="">
</div>
Notice how we’ve moved the event-driven behaviour from the “weather” element to the “specify” element. We’re telling it to listen for the change event on the “weather” component, and when “its value” is “other” then “show me”, where “me” refers to the element to which the hyperscript is attached.
What is most interesting here is how the context in which we are working remains clear: “its” refers to the “weather” element and “me” refers to the “specify” element, just as it would in an English sentence.
This demonstrates the flexible event-handling and natural-language benefits of hyperscript really well. It also helps to eliminate the confusion that every developer has encountered with the this
keyword in JavaScript. To make this possible, hyperscript performs the impressive task of understanding pronouns (I/me/myself, you/yourself, it) and possessive expressions (my/your/its).
Reusability #
I was sceptical of hypersript at first, but I was also sceptical of Tailwind CSS and it has since become my go-to CSS framework. What appeals to me the most is the concise, inline, event-driven, no-JS (unless you need it) approach.
<button _="on click toggle .hidden on #menu"
class="rounded bg-black text-white p-4 sm:hidden"
>
☰
</button>
<button _="on click toggle .hidden on #menu"
class="rounded bg-black text-white p-4 hidden sm:block"
>
Menu
</button>
<div id="menu">
...
</div>
If you find yourself reusing the same hyperscript then, just like you can extract component classes with @apply in Tailwind CSS, you can bundle hyperscript into reusable behaviors that you then “install” on elements.
<script type="text/hyperscript">
behavior ToggleMenu
on click toggle .hidden on #menu
end
</script>
<button _="install ToggleMenu" class="button sm:hidden">
☰
</button>
<button _="install ToggleMenu" class="button hidden sm:block">
Menu
</button>
<div id="menu">
...
</div>
In addition to behaviors, you can define hyperscript functions, which are a lower-level construct. While behaviors are applied, functions are executed. hyperscript also allows you to reach for vanilla JS if and when you ever need it.
Complexity made Simple #
We’ve only scratched the surface of what hyperscript can do. While most of the time it will be used to sprinkle interactive behaviour around your markup (I wouldn’t suggest writing an SPA in it), hyperscript has some powerful features that significantly simplify what would otherwise be rather complex or verbose to achieve.
Async transparency allows you to write asynchronous hyperscript that will behave in a synchronous, linear manner. The classic example of this is the Fetch API, which is most commonly used with a Promise.
//--- VanillaJS ---
fetch('/search')
.then(response => response.text())
.then(response => () => {
document.getElementById('result').innerHTML = response;
});
Because the fetch
method returns a Promise instead of the response directly, we need to first understand and then implement promises in order to insert the response into the “result” element.
With hyperscript, we just write everything out linearly, and it works just like it was a synchronous request.
<button _="on click fetch /search then put it into #result">
Search
</button>
<div id="result"></div>
Web Workers, Web Sockets and Server Sent Events are all supported by hyperscript, so you can do some very neat things without having to learn complex JavaScript APIs. Whether you would want to is another question!
Using hyperscript #
Using hyperscript is as simple as dropping a script
tag into your HTML and off you go, no build step required. Also worth mentioning is the fact that hyperscript works seamlessly with htmx, by the same author, and by extension with our very own Sprig.
The hyperscript documentation is a good starting point but feels like it could use some improving, along with the cookbook and tooling such as the playground and a debugger. What I would really like to eventually see is an autosuggest/autocomplete tool that can be used for exploratory learning and to help write more correct hyperscript more quickly.
hyperscript is approaching a 1.0 release and based on what I know of the core developer, I expect it to remain stable for a long time. Personally, I plan on using hyperscript on projects as soon as it reaches a stable version, especially in combination with a templating language such as Twig when working with Craft CMS.
Closing Thoughts #
Unlike reading hyperscript, learning to write hyperscript is not trivial. It is, after all, a language with verbs, nouns, pronouns, imperatives and possessive adjectives. And yet it is not English. Fortunately, hyperscript will tell you about errors in your code via the browser console (most of the time).
“Learning a new language is not only learning how to do things with different words, but also learning how to do things in a different world.”
hyperscript feels like it would be suitable for front-end developers of all experience levels, provided it is used as the right tool for the job. For less experienced developers, it will be more approachable than vanilla JS and JavaScript frameworks that require a programmatic mindset. For more experienced developers, it allows for cutting down on code, using advanced JavaScript APIs with a simpler syntax, and writing elegant, reusable scripts.
Overall I like hyperscript’s unique approach to getting stuff done on the front-end. There is a learning curve when trying to do more advanced things, but its conciseness and self-documenting nature is very appealing. And when used with care and precision, hyperscript can read just like poetry.
<div _="on load append 'The end.' to me"></div>
The end.