Introduction

Hyperscript is a scripting language for doing front end web development. It is designed to make it very easy to respond to events and do simple DOM manipulation in code that is directly embedded on elements on a web page.

Here is a simple example of some hyperscript:

Example
<button _="on click toggle .red on me">
Click Me
</button>

The first thing to notice is that hyperscript is defined directly on the button, using the _ (underscore) attribute.

Embedding code directly on the button like this might seem strange at first, but hyperscript is one of a growing number of technologies that de-emphasize Separation of Concerns in favor of Locality of Behavior.

Other examples of libraries going this direction are Tailwind CSS, AlpineJS and htmx.

The next thing you will notice about hyperscript is its syntax, which is very different than most programming languages used today. Hyperscript is part of the xTalk family of scripting languages, which ultimately derive from HyperTalk. These languages all read more like english than the programming languages you are probably used to.

This unusual syntax has advantages, once you get over the initial shock:

Hyperscript favors read time over write time when it comes to code. It can be a bit tricky to write at first for some people who are used to other programming languages, but it reads very clearly once you are done.

Code is typically read many more times than it is written, so this tradeoff is a good one for simple front end scripting needs.

Below you will find an overview of the various features, commands and expressions in hyperscript, as well as links to more detailed treatments of each them.

Some other hypserscript resources you may want to check out are:

OK, let's get started with hyperscript!

Install & Quick Start

Hyperscript is a dependency-free JavaScript library that can be included in a web page without any build step:

<script src="https://unpkg.com/hyperscript.org@0.9.13"></script>

If you are using a build step:

import _hyperscript from 'hyperscript.org';

_hyperscript.browserInit();

After you've done this, you can begin adding hyperscript to elements:

<div _="on click call alert('You clicked me!')">
  Click Me!
</div>

You can also add hyperscript within script tags that are denoted as text/hyperscript:

<script type="text/hyperscript">
  on mousedown
    halt the event -- prevent text selection...
    -- do other stuff...
  end
</script>

Features defined in script tags will apply to the body.

Hyperscript has an open, pluggable grammar & some advanced features do not ship by default (e.g. workers).

To use a feature like workers you can either:

Language Basics

A hyperscript script consists of a series of "features", the most common of which is an event handler, as we saw in the first example. The body of a feature then consists of a series of "commands", which are often called statements in other languages. These commands may include one or more "expressions".

Going back to our original example:

<button _="on click toggle .red on me">
  Click Me
</button>

In the script above:

All hyperscript scripts are made up of these basic building blocks.

It's worth mentioning that, if you prefer, you can use script or data-script instead of _ when using hyperscript:

Example
<button script="on click toggle .red on me">
Click Me
</button>

Comments

Comments in hyperscript start with the -- characters and a whitespace character (space, tab, carriage return or newline) and go to the end of the line:

-- this is a comment
log "Yep, that was a comment"

To ease migrations to hyperscript, // and /* ... */ comments are supported.

Separators

Multiple commands may be optionally separated with a then, which acts like a semi-colon in JavaScript:

log "Hello" then log "World"

Using the then keyword is recommended when multiple commands are on the same line.

When commands have bodies that include other commands, such as with the if command, the series of commands are terminated by an end:

if x > 10  -- start of the conditional block
  log "Greater than 10"
end        -- end of the conditional block

Features are also terminated by an end:

on click
  log "Clicked!"
end

The end terminator can often be omitted for both features and statements if either of these conditions hold:

In practice, end is used only when necessary, in order to keep scripts small and neat.

Expressions

Many expressions in hyperscript will be familiar to developers and are based on expressions available in JavaScript:

Others are a bit more exotic and, for example, make it easy to work with the DOM:

We will see how features, commands and expressions all fit together and what they can do in the coming sections.

Variables

In hyperscript, variables are created by the set or put commands, with set being preferred.

Here is how you create a simple, local variable:

set x to 10

Here is an example that creates a local variable and then logs it to the console:

Example: Local variable
<button _="on click set x to 10 then log x">
Click Me
</button>

If you click this button and open up the console, you should see 10 being logged to it.

Scoping

hyperscript has three different variable scopes: local, element, and global.

Note that hyperscript has a flat local scope, similar to JavaScript's var statement.

Variable Names & Scoping

In order to make non-locally scoped variables easy to create and recognize in code, hyperscript supports the following naming conventions:

By using these prefixes it is easy to tell differently scoped variables from one another without a lot of additional syntax:

set $foo to 10 -- sets a global named $foo
set :bar to 20 -- sets an element scoped variable named :bar

Here is an example of a click handler that uses an element scoped variable to maintain a counter:

Example
<button _="on click increment :x then put it into the next <output/>">
Click Me
</button>
<output>--</output>
--

This script also uses the implicit it symbol, which we will discuss below.

Scoping Modifiers

You may also use scope modifiers to give symbols particular scopes:

Attributes

In addition to scoped variables, another way to store data is to put it directly in the DOM, in an attribute of an element.

You can access attributes on an element with the attribute literal syntax, using an @ prefix:

set @my-attr to 10

This will store the value 10 in the attribute my-attr on the current element:

<div my-attr="10"></div>

Note that, unlike regular variables, attributes can only store strings. Anything else you store in them will be converted to a string.

You can remember the @ sign as the attribute operator. We will discuss other DOM literals below.

Here is the above example, rewritten to use an attribute rather than an element-scoped variable:

Example
<button _="on click increment @my-attr then put it into the next <output/>">
Click Me
</button>
<output>--</output>
--

If you click the above button a few times and then inspect it using your browsers developer tools, you'll note that it has a my-attr attribute on it that holds a string value of the click count.

The increment command is discussed below.

Special Names & Symbols

One of the interesting aspects of hyperscript is its use of implicit names for things, often with multiple ways to refer to the same thing. This might sound crazy, and it kind of is, but it helps to make scripts much more readable!

We have already seen the use of the it symbol above, to put the result of an increment command into an element.

It turns out that it is an alias for result, which we could have used instead:

Example: It
<button _="on click increment :x then put result into the next <output/>">
Click Me
</button>
<output>--</output>
--

It may be equivalent, but it doesn't read as nicely does it?

That's why hyperscript supports the it symbol as well.

Another funny thing you might have noticed is the appearance of the in this script.

the is whitespace before any expression in hyperscript and can be used to make your code read more nicely.

For example, if we wanted to use result rather than it, we would write the result instead, which reads more nicely:

Example: The
<button _="on click increment :x then put the result into the next <output/>">
Click Me
</button>
<output>--</output>
--

This is exactly equivalent to the previous example, but reads better. Hyperscript is all about readability!

In this case, we'd probably stick with it :)

The Hyperscript Zoo

In addition to result and it, hyperscript has a number of other symbols that are automatically available, depending on the context, that make your scripting life more convenient.

Here is a table of available symbols:

result it its

the result of the last command, if any (e.g. a call or fetch)

me my I

the element that the current event handler is running on

event

the event that triggered the event current handler, if any

body

the body of the current document, if any

target

the target of the current event, if any

detail

the detail of the event that triggered the current handler, if any

sender

the element that sent the current event, if any

Note that the target is the element that the event originally occurred on.

Event handlers, discussed below, may be placed on parent elements to take advantage of event bubbling which can reduce redundancy in code.

Logging To The Console

If you wish to print something to the console you can use the log command:

log "Hello Console!"

Simplicity itself.

Objects

Hyperscript is not an object-oriented language: it is, rather, event-oriented. However it still allows you to work with objects in an easy and convenient manner, which facilitates interoperability with all the functionality of JavaScript, including the DOM APIs, JavaScript libraries and so on.

Here is how you can work with objects in hyperscript:

Properties

Hyperscript offers a few different ways to access properties of objects. The first two should be familiar to JavaScript developers:

set x to {name : "Joe", age: 35}    -- create an object with some properties
log x.name                          -- standard "dot" notation
log x['name']                       -- standard array-index notation

The next mechanism is known as a possessive expression and uses the standard english 's to express a property access:

set x to {name : "Joe", age: 35}    -- create an object with some properties
log x's name                        -- access the name property using a possessive

There are two special cases for the possessive expression, the symbols my and its, both of which can be used without the 's for possessive expressions:

get the first <div/> then          -- get the first div in the DOM, setting the `results` variable
set my innerHTML to its innerHTML  -- use possessive expressions to set the current elements innerHTML
                                   -- to the innerHTML of that div

Finally, you can also use the of expression to get a property as well:

set x to {name : "Joe", age: 35}    -- create an object with some properties
log the name of x                   -- access the name property using an of expression

The of operator flips the order of the property & the element that the property is on, which can sometimes clarify your code.

Which of these options you choose for property access is up to you. We recommend the possessive form in most cases as being the most "hyperscripty", with the of form being chosen when it helps to clarify some code by putting the final property at the front of the expression.

Flat Mapping

Inspired by jQuery, another feature of property access in hyperscript is that, when a property of an Array-like object is accessed, it will flat-map the results to a single, linear array of that property applied to all values within the array.

set allDivs to <div/>                      -- get all divs
set allParents to the parent of allDivs    -- get all parents of those divs as an array
set allChildren to the children of allDivs -- get all children of those divs as an array

On an array, only the length property will not perform a flat map in this manner.

Null Safety

Finally, all property accesses in hyperscript are null safe, so if the object that the property is being accessed on is null, the result of the property access will be null as well, without a need to null-check:

set example to null
log example.prop     -- logs null, because `example` is null

This null-safe behavior is appropriate for a scripting language intended for front-end work.

Creating New Objects

If you want to make new objects, you can use the make command:

make a URL from "/path/", "https://origin.example.com"

Which is equal to the JavaScript new URL("/path/", "https://origin.example.com")

If you wish to assign an identifier to the new object you can use the called modifier:

make a URL from "/path/", "https://origin.example.com" called myURL
log myURL

You can also use query literals, discussed below, to create new HTML content:

make an <a.navlink/> then put it after me

Arrays

Hyperscript arrays work very similarly to JavaScript arrays:

set myArr to [1, 2, 3]
log myArr[0]           -- logs "1"

You can use the first, last and random keywords, discussed below, with arrays:

set myArr to [1, 2, 3]
log the first of myArr  -- logs "1"
log the last of myArr   -- logs "3"
log random in myArr     -- logs a random element from the array
Closures

Hyperscript does not encourage the use of closures or callbacks nearly as much as JavaScript. Rather, it uses async transparency to handle many of the situations in which JavaScript would use them.

However, there is one area where closures provide a lot of value in hyperscript: data structure manipulation. The hyperscript syntax for closures is inspired by haskell, starting with a \ character, then the arguments, then an arrow ->, followed by an expression:

set strs to ["a", "list", "of", "strings"]
set lens to strs.map( \ s -> s.length )
log lens

Control Flow

Conditional control flow in hyperscript is done with the if command or the unless modifier. The conditional expression in an if statement is not parenthesized. Hyperscript uses end rather than curly-braces to delimit the conditional body.

The else-branch can use either the else keyword or the otherwise keyword.

Example: "If" command
<button _="on click increment :x
            if :x <= 3
              put :x into the next <output/>
            else
              put '3 is the max...' into the next <output/>
            end">
Click Me
</button>
<output>--</output>

--

As mentioned in the introduction, end is often omitted when it isn't needed in order to make scripts smaller:

<button _="on click increment :x
              if :x < 3
                put :x into the next <output/>
              otherwise
                put '3 is the max...' into the next <output/>">
  Click Me
</button>
<output>--</output>

You can chain if/else commands together in the usual manner.

All commands also support an unless modifier to conditionally execute them. This allows for a very succinct way of expressing branching logic.

<button _="on click set error to functionCouldReturnError()
              log error unless no error">
  Log Result
</button>

See in the following example how the .bordered class is used to alter the behaviour of the second button.

Example: "unless" modifier
<button _="on click toggle .bordered on #second-button">
Toggle Next Border
</button>
<button id="second-button"
        _="on click toggle .red unless I match .bordered">
Toggle My Background
</button>

Comparisons & Logical Operators

In addition to the usual comparison operators from JavaScript, such as == and !=, hyperscript supports a rich set of natural language style comparisons for use in if commands:

A small sampling is shown below:

a is b

Same as a == b.

a is not b

Same as a != b.

no a

Same as a == null or a == undefined or [[a.length]] == 0.

element matches selector

Does a CSS test, i.e. if I match .selected.

a exists

Same as not (no a).

x is greater than y
x is less than y

Same as > and <, respectively.

collection is empty

Tests if a collection is empty.

If the left hand side of the operator is I, then is can be replaced with am:

get chosenElement()
if I am the result then remove me end

Using these natural language alternatives allows you to write very readable scripts.

Comparisons can be combined via the and, or and not expressions in the usual manner:

if I am <:checked/> and the closest <form/> is <:focus/>
  add .highlight to the closest <form/>

Loops

The repeat command is the looping mechanism in hyperscript.

It supports a large number of variants, including a short hand for version:

-- a basic for loop
repeat for x in [1, 2, 3]
  log x
end

-- you may omit the 'repeat' keyword for a for loop
for x in [1, 2, 3]
  log x
end

-- you may repeat without an explicit loop variable and use
-- the implicit `it` symbol
repeat in [1, 2, 3]
  log it
end

-- you may use a while clause to loop while a condition is true
repeat while x < 10
  log x
end

-- you may use an until clause to loop until a condition is true
repeat until x is 10
  log x
end

-- you may use the times clause to repeat a fixed number of times
repeat 3 times
  log 'looping'
end

-- you may use the index clause on any of the above
-- to bind the loop index to a given symbol
for x in [1, 2, 3] index i
  log i, "is", x
end

-- you can loop forever if you like
repeat forever
  if I match :focus
    break
  end
  wait 2s
end

Loops support both the break and continue commands.

You can also use events to signal when a loop ends, see the async section on loops

Aggregate Operations

Note that loops are often not required in hyperscript. Many commands will automatically deal with arrays and collections for you.

For example, if you want to add the class .foo to all elements that have the class .bar on it, you can simply write this:

add .foo to .bar

The add command will take care of looping over all elements with the class .bar.

No need to loop explicitly over the results.

Math Operations

Hyperscript supports most of the regular math operators:

set x to 10
set y to 20
set sum to x + y
set diff to x - y
set product to x * y

with one exception, the modulo operator uses the keyword mod:

set x to 10 mod 3

Hyperscript does not have a notion of mathematical operator precedence. Instead, math operators must be fully parenthesized when used in combination with other math operators:

set x to 10
set y to 20
set sumOfSquares to (x * x) + (y * y)

If you did not fully parenthesize this expression it would be a parse error.

This clarifies any mathematical logic you are doing and encourages simpler expressions, which, again helps readability.

Hyperscript also offers an increment and decrement command for modifying numbers:

set x to 1
increment x
puts x -- prints 2 to the console

A nice thing about the increment and decrement commands is that they will automatically handle string to number conversions and, therefore, can be used with numbers stored in attributes on the DOM:

on click
   increment @data-counter
   if @data-counter as Int is greater than 4
     add @disabled -- disable after the 5th click

Strings

Hyperscript supports strings that use either a single quotes or double quotes:

set hello to 'hello'
set world to "world"
set helloWorld to hello + " " + world

and also supports JavaScript style template strings:

set helloWorld to `${hello} ${world}`

The append command can append content to strings (as well as to arrays and the DOM):

  get "hello"      -- set result to "hello"
  append " world"  -- append " world" to the result
  log it           -- log it to the console

Conversions

To convert values between different types, hyperscript has an as operator:

Example: Converting Values
<button _="on click
             get the (value of the next <input/>) as an Int
             increment it
             put it into the next <output/>">
Add 1 To :
</button>
<input/>
<output>--</output>

--

Here we get the input value, which is a String, and we convert it to an Integer. Note that we need to use parenthesis to ensure that the as expression does not bind too tightly.

We then increment the number and put it into the next output element.

Out of the box hyperscript offers a number of useful conversions:

You can also add your own conversions to the language as well.

Calling Functions

There are many ways to invoke functions in hyperscript. Two commands let you invoke a function and automatically assign the result to the result variable: call and get:

call alert('hello world!')
get the nextInteger() then log it -- using the 'it' alias for 'result`

You can also invoke functions as stand-alone commands:

log "Getting the selection"
getSelection()
log "Got the selection"
log it

Finally, you can use the pseudo-command syntax, which allows you to put the method name first on the line in a method call, to improve readability in some cases:

reload() the location of the window
writeText('evil') into the navigator's clipboard
reset() the #contact-form

These are called "pseudo-commands" because this syntax makes method calls look like a normal command in hyperscript.

Events & Event Handlers

Events are at the core of hyperscript, and event handlers are the primary entry point into most hyperscript code.

hyperscript's event handlers allow you to respond to any event (not just DOM events, as with onClick handlers) and provide a slew of features for making working with events easier.

Here is an example:

Example: Event Handlers
<style>
.clicked::after {
content: " ... Clicked!"
}
</style>
<button _="on click add .clicked">
Add The "clicked" Class To Me
</button>

The script above, again, found on the _ attribute, does, well, almost exactly what it says:

On the 'click' event for this button, add the 'clicked' class to this button

This is the beauty of hyperscript: you probably knew what it was doing immediately, when reading it.

Event handlers have a very extensive syntax that allows you to, for example:

You can read all the gory details on the event handler page, but chances are, if you want some special handling of an event, hyperscript has a nice, clear syntax for doing so.

Event Queueing

By default, the event handler will be run synchronously, so if the event is triggered again before the event handler finished, the new event will be queued and handled only when the current event handler finishes.

You can modify this behavior in a few different ways:

The Every Modifier

An event handler with the every modifier will execute the event handler for every event that is received, even if the preceding handler execution has not finished.

<button _="on every click add .clicked">
  Add The "clicked" Class To Me
</button>

This is useful in cases where you want to make sure you get the handler logic for every event going immediately.

The Queue Modifier

The every keyword is a prefix to the event name, but for other queuing options, you postfix the event name with the queue keyword.

You may pick from one of four strategies:

queue last is the default behavior

Example: Queue All
<button _="on click queue all
                  increment :count
                  wait 1s then put it into the next <output/>">
Click Me Quickly...
</button>
<output>--</output>

--

If you click quickly on the button above you will see that the count slowly increases as each event waits 1 second and then completes, and the next event that has queued up executes.

Event Destructuring

You can destructure properties found either on the event or in the event.detail properties by appending a parenthesized list of names after the event name.

This will create a local variable of the same name as the referenced property:

Example: Event Parameters
<button _="on mousedown(button) put the button into the next <output/>">
Click Me With Different Buttons...
</button>
<output>--</output>
--

Here the event.button property is being destructured into a local variable, which we then put into the next output element

Event Filters

You can filter events by adding a bracketed expression after the event name and destructured properties (if any).

The expression should return a boolean value true if the event handler should execute.

Note that symbols referenced in the expression will be resolved as properties of the event, then as symbols in the global scope.

This lets you, for example, test for a middle click on the click event, by referencing the button property on that event directly:

Example: Event Filters
<button _="on mousedown[button==1] add .clicked">
  Middle-Click To Add The "clicked" Class To Me
</button>

Halting Events

An event handler can exit with the halt command. By default this command will halt the current event bubbling, call preventDefault() and exit the current event handlers. However, there are forms available to stop only the event from bubbling, but continue on in the event handler:

<script type="text/hyperscript">
  on mousedown
    halt the event -- prevent text selection...
    -- do other stuff...
  end
</script>

You may also use the exit command to exit a function, discussed below.

Sending Events

hyperscript not only makes it easy to respond to events, but also makes it very easy to send events to other elements using the send and trigger commands. Both commands do the same thing: sending an event to an element (possibly the current element!) to handle.

Here are a few examples:

Example: Send, Trigger
<button _="on click send foo to the next <output/>">Send Foo</button>
<button _="on click trigger bar on the next <output/>">Send Bar</button>
<output _="on foo put 'I got a foo event!' into me
         on bar put 'I got a bar event!' into me">
No Events Yet...
</output>

No Events Yet...

You can also pass arguments to events via the event.detail property, and use the destructuring syntax discussed above to parameterize events:

Example: Send with arguments
<button _="on click send showMessage(message:'Foo!') to the next <output/>">Send Foo</button>
<button _="on click send showMessage(message:'Bar!') to the next <output/>">Send Bar</button>
<output _="on showMessage(message) put `The message '${message}' was sent to me` into me">
No Events Yet...
</output>

No Events Yet...

As you can see, working with events is very natural in hyperscript. This allows you to build clear, readable event-driven code without a lot of fuss.

Synthetic Events

hyperscript includes a few synthetic events that make it easier to use more complex APIs in JavaScript.

Mutation Events

You can listen for mutations on an element with the on mutation form. This will use the Mutation Observer API, but will act more like a regular event handler.

<div _='on mutation of @foo put "Mutated" into me'></div>

This div will listen for mutations of the foo attribute on this div and, when one occurs, will put the value "Mutated" into the element.

Here is a div that is set to content-editable='true' and that listens to mutations and updates a mutation count below:

Example: Content Editable Mutations
<div contenteditable="true"
   _="on mutation of anything increment :mutationCount then put it into the next <output/>">
Hello World
</div>
<output>--</output>
Hello World
--
Intersection Events

Another synthetic event is the intersection event that uses the Intersection Observer API. Again, hyperscript makes this API feel more event-driven:

<img _="on intersection(intersecting) having threshold 0.5
        if intersecting transition opacity to 1
        else transition opacity to 0 "
    src="https://placebear.com/200/300"/>

This image will become visible when 50% or more of it has scrolled into view. Note that the intersecting property is destructured into a local symbol, and the having threshold modifier is used to specify that 50% of the image must be showing.

Here is a demo:

Init Blocks

If you have logic that you wish to run when an element is initialized, you can use the init block to do so:

<div _="init transition my opacity to 100% over 3 seconds">
  Fade Me In
</div>

The init keyword should be followed by a set of commands to execute when the element is loaded.

Functions

Functions in hyperscript are defined by using the def keyword.

Functions defined on elements will be available to the element the function is defined on, as well as any child elements.

Functions can also be defined in a hyperscript script tag:

<script type="text/hyperscript">
  def waitAndReturn()
    wait 2s
    return "I waited..."
  end
</script>

This will define a global function, waitAndReturn() that can be invoked from anywhere in hyperscript.

Hyperscript can also be loaded remotely in ._hs files.

When loaded in this manner, the script tags must appear before loading hyperscript:

<script type="text/hyperscript" src="/functions._hs"></script>
<script src="https://unpkg.com/hyperscript.org"></script>

Hyperscript is fully interoperable with JavaScript, and global hyperscript functions can be called from JavaScript as well as vice-versa:

var str = waitAndReturn();
str.then(function(val){
  console.log("String is: " + val);
})

Hyperscript functions can take parameters and return values in the expected way:

<script type="text/hyperscript">
  def increment(i)
    return i + 1
  end
</script>

You may exit a function using return if you wish to return a value or exit if you do not want to return a value.

Namespacing

You can namespace a function by prefixing it with dot separated identifiers. This allows you to place functions into a specific namespace, rather than polluting the global namespace:

<script type="text/hyperscript">
  def utils.increment(i)
    return i + 1
  end
</script>
<script>
  console.log(utils.increment(41)); // access it from JavaScript
</script>

Exception Handling

Both functions and event handlers may have a catch block associated with them:

def example
  call mightThrowAnException()
catch e
  log e
end

on click
  call mightThrowAnException()
catch e
  log e
end

This allows you to handle exceptions that occur during the execution of the function or event handler.

If you do not include a catch block on an event handler and an uncaught exception occurs, an exception event will be triggered on the current element and can be handled via an event handler, with the error set to the message of the exception:

on exception(error)
     log "An error occurred: " + error

Note that exception handling in hyperscript respects the async-transparent behavior of the language.

Finally Blocks

Both functions and event handlers also support a finally block to ensure that some cleanup code is executed:

on click
  add @disabled to me
  fetch /example
  put the result after me
finally
  remove @disabled from me

In this code we ensure that the disabled property is removed from the current element.

Throwing Exceptions

You may throw an exception using the familiar throw keyword:

on click
  if I do not match .selected
    throw "I am not selected!"
  ...

Working With The DOM

The primary use case for hyperscript is adding small bits of interactivity to the DOM and, as such, it has a lot of syntax for making this easy and natural.

We have glossed over a lot of this syntax in previous examples (we hope it was intuitive enough!) but now we will get into the details of what they all do:

Finding Elements

There are two sides to DOM manipulation: finding stuff and mutating it. In this section we will focus on how to find things in the DOM.

DOM Literals

You are probably used to things like number literals (e.g. 1) or string literals (e.g. "hello world").

Since hyperscript is designed for DOM manipulation, it supports special literals that make it easy to work with the DOM.

Some are inspired by CSS, while others are our own creation.

Here is a table of the DOM literals:

.class name
.{expression}

A class literal starts with a . and returns all elements with that class.

#ID
#{expression}

An ID literal starts with a # and returns the element with that id.

<css selector />

A query literal is contained within a < and />, returns all elements matching the CSS selector.

@attribute name

An attribute literal starts with an @ (hence, attribute, get it?) and returns the value of that attribute.

*style property

A style literal starts with an * (a reference to CSS Tricks) and returns the value of that style property.

1em
0%
expression px

A measurement literal is an expression followed by a CSS unit, and it appends the unit as a string. So, the above expressions are the same as "1em", "0%" and `${expression}px`.

Here are a few examples of these literals in action:

-- adds the 'disabled' class to the element with the id 'myDiv'
add .disabled to #myDiv

-- adds the 'highlight' class to all divs with the class 'tabs' on them
add .highlight to <div.tabs/>

-- sets the width of the current element to 35 pixels
set my *width to 35px

-- adds the `disabled` attribute to the current element
add @disabled to me

Class literals, ID Literals and Query Literals all support a templating syntax.

This allows you to look up elements based on a variable rather than a fixed value:

-- adds the 'disabled' class to the element with the id 'myDiv'
set idToDisable to 'myDiv'
add .disabled to #{idToDisable}

-- adds the 'highlight' class to all elements with the 'tabs' class
set classToHighlight to 'tabs'
add .highlight to .{classToHighlight}

-- removes all divs w/ class .hidden on them from the DOM
set elementType to 'div'
remove <${elementType}.hidden/>

All these language constructs make it very easy to work with the DOM in a concise, enjoyable manner.

Compare the following JavaScript:

document.querySelector('#example-btn')
  .addEventListener('click', e => {
    document.querySelectorAll(".elements-to-remove").forEach(value => value.remove());
})

with the corresponding hyperscript:

on click from #example-btn
  remove .elements-to-remove

You can see how the support for CSS literals directly in hyperscript makes for a much cleaner script, allowing us to focus on the logic at hand.

Finding Things In Other Things

Often you want to find things within a particular element. To do this you can use the in expression:

-- add the class 'highlight' to all paragraph tags in the current element
add .highlight to <p/> in me

Finding The Closest Matching (Parent) Element

Sometimes you wish to find the closest element in a parent hierarchy that matches some selector. In JavaScript you might use the closest() function

To do this in hyperscript you can use the closest expression:

-- add the class 'highlight' to the closest table row to the current element
add .highlight to the closest <tr/>

Note that closest starts with the current element and recurses up the DOM from there. If you wish to start at the parent instead, you can use this form:

-- add the class 'highlight' to the closest div to the current element, excluding the current element
add .highlight to the closest parent <div/>

Finding Things By Position

You can use the positional expressions to get the first, last or a random element from a collection of things:

-- add the class 'highlight' to the first paragraph tag in the current element
add .highlight to the first <p/> in me

Finding Things Relative To Other Things

You can use the relative positional expressions next and previous to get an element relative to either the current element, or to another element:

-- add the class 'highlight' to the next paragraph found in a forward scan of the DOM
add .highlight to the next <p/>

Note that next and previous support wrapping, if you want that.

Updating The DOM

Using the expressions above, you should be able to find the elements you want to update easily.

Now, on to updating them!

Set & Put

The most basic way to update contents in the DOM is using the set and put commands. Recall that these commands can also be used to set local variables.

When it comes to updating DOM elements, the put command is much more flexible, as we will see.

First, let's just set the innerHTML of an element to a string:

Example: Setting innerHTML
<button _="on click set my innerHTML to 'Clicked!'">
Click Me
</button>

Using the put command would look like this:

Example: Setting properties with "put"
<button _="on click put 'Clicked!' into my innerHTML">
Click Me
</button>

In fact, the put command is smart enough to default to innerHTML when you put something into an element, so we can omit the innerHTML entirely:

Example: Putting things into elements
<button _="on click put 'Clicked!' into me">
Click Me
</button>

The put command can also place content in different places based on how it is used:

Example: Put X before Y
<button _="on click put 'Clicked!' before me">
Click Me
</button>

The put command can be used in the following ways:

put content before element

Puts the content in front of the element, using Element.before.

put content at the start of element

Puts the content at the beginning of the element, using Element.prepend.

put content at the end of element

Puts the content at the end of the element, using Element.append.

put content after element

Puts the content after the element, using Element.after.

This flexibility is why we generally recommend the put command when updating content in the DOM.

Setting Attributes

One exception to this rule is when setting attributes, which we typically recommend using set.

It just reads better to us:

Example: Setting attributes
<button _="on click set @disabled to 'disabled'">
Disable Me
</button>

set is recommended for setting values into normal variables as well.

Add, Remove & Toggle

A very common operation in front end scripting is adding or removing classes or attributes from DOM elements. hyperscript supports the add, remove and toggle commands do help do this.

Here are some examples adding, removing and toggling classes:

Example: "add" command
<button _="on click add .red to me">
Click Me
</button>
Example: "remove" command
<button class="red" _="on click remove .red from me">
Click Me
</button>
Example: "toggle" command
<button _="on click toggle .red on me">
Click Me
</button>

You can also add, remove and toggle attributes as well. Here is an example:

Example: Toggle an attribute
<button _="on click toggle @disabled on #say-hello">
Toggle Disabled State
</button>
<button id="say-hello" _="on click alert('hello!')">
Say Hello
</button>

Finally, you can toggle the visibility of elements by toggling a style literal:

Example: Toggle an attribute
<button _="on click toggle the *display of the next <p/>">
Toggle The Next Paragraph
</button>
<p>
Hyperscript is rad!
</p>

Hyperscript is rad!

Removing Content

You can also use the remove command to remove content from the DOM:

Example: Remove an element
<button _="on click remove me">
Remove Me
</button>

The remove command is smart enough to figure out what you want to happen based on what you tell it to remove.

Showing & Hiding Things

You can show and hide things with the show and hide commands:

Example: Show, Hide
<button _="on click
             hide me
             wait 2s
             show me">
             Peekaboo
</button>

By default, the show and hide commands will use the display style property. You can instead use visibility or opacity with the following syntax:

Example: Show/hide strategies
<button _="on click
             hide me with *opacity
             wait 2s
             show me with *opacity">
             Peekaboo
</button>

You can also apply a conditional to the show command to conditionally show elements that match a given condition by using a when clause:

Example: Filter elements with `show ... when`
<input _="on keyup show <li/> in #color-list
                   when its innerHTML contains my value">
<ul id="color-list">
<li>Red</li>
<li>Blue</li>
<li>Blueish Green</li>
<li>Green</li>
<li>Yellow</li>
</ul>

  • Red
  • Blue
  • Blueish Green
  • Green
  • Yellow

We mentioned this above, but as a reminder, you can toggle visibility using the toggle command:

Example: Toggle visibility
<button _="on click toggle the *display of the next <p/>">
Toggle The Next Paragraph
</button>
<p>
Hyperscript is rad!
</p>

Hyperscript is rad!

Transitions

You can transition a style from one state to another using the transition command. This allows you to animate transitions between different states:

Example: "transition" command
<button _="on click transition my *font-size to 30px
             then wait 2s
             then transition my *font-size to initial">
Transition My Font Size
</button>

The above example makes use of the special initial symbol, which you can use to refer to the initial value of an elements style when the first transition begins.

Class-Based Transitions

The transition command is blocking: it will wait until the transition completes before the next command executes.

Another common way to trigger transitions is by adding or removing classes or setting styles directly on an element.

However, commands like add, set, etc. do not block on transitions.

If you wish to wait until a transition completes after adding a new class, you should use the settle command which will let any transitions that are triggered by adding or removing a class finish before continuing.

Example: Wait for transitions/animations to finish
<button style="transition: all 800ms ease-in"
       _="on click add .red then settle then remove .red">
Flash Red
</button>

If the above code did not have the settle command, the button would not flash red because the class .red would be added and then removed immediately

This would not allow the 800ms transition to .red to complete.

Measuring Things

Sometimes you want to know the dimensions of an element in the DOM in order to perform some sort of translation or transition. Hyperscript has a measure command that will give you measurement information for an element:

Example: Measure an Element
<button _="on click measure my top then
                  put `My top is ${top}` into the next <output/>">
Click Me To Measure My Top
</button>
<output>--</output>

--

You can also use the pseudo-style literal form *computed-<style property> to get the computed (actual) style property value for an element:

Example: Get A Styles Computed Value
<button _="on click get my *computed-width
                  put `My width is ${the result}` into the next <output/>">
Click Me To Get My Computed Width
</button>
<output>--</output>

--

Remote Content

Hyperscript is primarily designed for front end scripting, local things like toggling a class on a div and so on, and is designed to pair well with htmx, which uses a hypermedia approach for interacting with servers.

Broadly, we recommend that approach: you stay firmly within the original REST-ful model of the web, keeping things simple and consistent, and you can use hyperscript for small bits of front end functionality. htmx and hyperscript integrate seamlessly, so any hyperscript you return to htmx will be automatically initialized without any additional work on your part.

Fetch

However, there are times when calling out to a remote server is useful from a scripting context, and hyperscript supports the fetch command for doing so:

Example: Issue a Fetch Request
<button _="on click fetch /clickedMessage then
                  put the result into the next <output/>">
Fetch It
</button>
<output>--</output>

--

The fetch command uses the fetch API and allows you configure the fetch as you want, including passing along headers, a body, and so forth.

Additionally, you may notice that the fetch command, in contrast with the fetch() function, does not require that you deal with a Promise. Instead, the hyperscript runtime deals with the promise for you: you can simply use the result of the fetch as if the fetch command was blocking.

This is thanks to the async transparency of hyperscript, discussed below.

Going Places

While using ajax is exciting, sometimes you simply wish to navigate the browser to a new location. To support this hyperscript has a go command that allows you to navigate locally or to new URLs, depending on how it is used:

Example: Going Around The Document
<button _="on click
            go to the top of the body smoothly
            wait 2s
            go to the bottom of me smoothly">
            Take A Trip
</button>

You can also use it to navigate to another web page entirely:

Example: Going Elsewhere
<button _="on click go to url https://htmx.org">
            Go Check Out htmx
</button>

Async Transparency

One of the most distinctive features of hyperscript is that it is "async transparent". What that means is that, for the most part, you, the script writer, do not need to worry about asynchronous behavior. In the fetch section, for example, we did not need to use a .then() callback or an await keyword, as you would need to in JavaScript: we simply fetched the data and then inserted it into the DOM.

To make this happen, the hyperscript runtime handles Promises under the covers for you, resolving them internally, so that asynchronous behavior simply looks linear.

This dramatically simplifies many coding patterns and effectively decolors functions (and event handlers) in hyperscript.

Furthermore, this infrastructure allows hyperscript to work extremely effectively with events, allowing for event driven control flow, explained below.

Waiting

In JavaScript, if you want to wait some amount of time, you can use the venerable setTimeout() function:

console.log("Start...")
setTimeout(function(){
  console.log("Finish...")
}, 1000);

This code will print "Start" to the console and then, after a second (1000 milliseconds) it will print "Finish".

To accomplish this in JavaScript requires a closure, which acts as a callback. Unfortunately this API is awkward, containing a lot of syntactic noise and placing crucial information, how long the delay is, at the end. As this logic becomes more complex, that delay information gets further and further away from where, syntactically, the delay starts.

Contrast that with the equivalent hyperscript, which uses the wait command:

log "Start..."
wait 1s
log "Finish..."

You can see how this reads very nicely, with a linear set of operations occurring in sequence.

Under the covers, the hyperscript runtime is still using that setTimeout() API, but you, the script writer, are shielded from that complexity, and you can simply write linear, natural code.

This flexible runtime allows for even more interesting code. The wait command, for example, can wait for an event not just a timeout:

Example: Waiting On Events
<button _="on click put 'Started...' into the next <output/>
                    wait for a continue   -- wait for a continue event...
                    put 'Finished...' into the next <output/>
                    wait 2s
                    put '' into the next <output/>">
  Start
</button>
<button _="on click send continue to the previous <button/>">
  Continue
</button>
<output>--</output>

--

Now we are starting to see how powerful the async transparent runtime of hyperscript can be: with it you are able to integrate events directly into your control flow while still writing scripts in a natural, linear fashion.

Let's add a timeout to that previous example:

Example: Waiting On Events With A Timeout
<button _="on click put 'Started...' into the next <output/>
                  wait for a continue or 3s   -- wait for a continue event...
                  if the result's type is 'continue'
                    put 'Finished...' into the next <output/>
                  otherwise
                    put 'Timed Out...' into the next <output/>
                  end
                  wait 2s
                  put '--' into the next <output/>">
Start
</button>
<button _="on click send continue to the previous <button/>">
Continue
</button>
<output>--</output>

--

If you click the Continue button within 3 seconds, the wait command resume, setting the result to the event, so the result's type will be "continue".

If, on the other hand, you don't click the Continue button within 3 seconds, the wait command resume based on the timeout, setting the result to null, so the result's type will be null.

Toggling

Previously we looked at the toggle command. It turns out that it, too, can work with events:

Example: Toggle A Class With Events
<div _="on mouseenter toggle .red until mouseleave">
Mouse Over Me To Turn Me Red!
</div>
Mouse Over Me To Turn Me Red!

You can, of course, toggle the class on other elements, or toggle an attribute, or use different events: the possibilities are endless.

Loops

You can add async behavior to a loop by adding a wait command in the body, but loops can also have a loop condition based on receiving an event.

Consider this hyperscript:

Example: An Event-Driven Loop
<button class="pulsar"
      _="on click repeat until event stop
                  add .pulse then settle
                  remove .pulse then settle">
Click me to Pulse...
</button>
<button _="on click send stop to the previous <button/>">
Cancel
</button>

The loop will check if the given event, stop, has been received at the start of every iteration. If not, the loop will continue. This allows the cancel button to send an event to stop the loop.

However, note that the CSS transition is allowed to finish smoothly, rather than abruptly, because the event listener that terminates the loop is only consulted once a complete loop is made, adding and removing the class and settling cleanly.

The async keyword

Sometimes you do want something to occur asynchronously. Hyperscript provides an async keyword that will tell the runtime not to synchronize on a value.

So, if you wanted to invoke a method that returns a promise, say returnsAPromise() but not wait on it to return, you write code like this:

<button _="on click call async returnsAPromise() put 'I called it...' into the next <output/>">
  Get The Answer...
</button>

Hyperscript will immediately put the value "I called it..." into the next output element, even if the result from returnsAPromise() has not yet resolved.

Using JavaScript

Hyperscript is directly integrated with JavaScript, providing ways to use them side by side and migrate with ease.

Shared Comment Syntax

// and /* ... */ comments are supported, and ideal for migrating lines of code from JavaScript to Hyperscript "in-place". The multi-line comment may be used to "block out" code and write documentation comments.

Calling JavaScript

Any JavaScript function may be called directly from Hyperscript. See: calling functions.

<button _="on click call alert('Hello from JavaScript!')">
  Click me.
</button>

Inline JavaScript

Inline JavaScript may be defined using the js keyword.

<div _="init js alert('Hello from JavaScript!') end"></div>

Return values are supported.

<button _="on click js return 'Success!' end then put it into my.innerHTML">
 Click me.
</button>

Parameters are supported.

<button _="on click set foo to 1 js(foo) alert('Adding 1 to foo: '+(foo+1)) end">
 Click me.
</button>

JavaScript at the top-level may be defined using the same js command, exposing it to the global scope.

You may use inline JavaScript for performance reasons, since the Hyperscript runtime is more focused on flexibility, rather than performance.

This feature is useful in workers, when you want to pass JavaScript across to the worker's implementation:

<script type="text/hyperscript">
  worker CoinMiner
    js
      function mineNext() {
        // a JavaScript implementation...
      }
    end
    def nextCoin()
      return mineNext()
    end
  end
</script>

Advanced Features

We have covered the basics (and not-so-basics) of hyperscript. Now we come to the more advanced features of the language.

Behaviors

Behaviors allow you to bundle together some hyperscript code (that would normally go in the _ attribute of an element) so that it can be "installed" on any other. They are defined with the behavior keyword:

behavior Removable
  on click
    remove me
  end
end

They can accept arguments:

behavior Removable(removeButton)
  on click from removeButton
    remove me
  end
end

They can be installed as shown:

<div class="banner" _="install Removable(removeButton: #close-banner)">
  ...

For a better example of a behavior, check out Draggable._hs.

Web Workers

WebWorkers can be defined inline in hyperscript by using the worker keyword.

The worker does not share a namespace with other code, it is in its own isolated sandbox. However, you may interact with the worker via function calls, passing data back and forth in the normal manner.

<script type="text/hyperscript">
  worker Incrementer
    def increment(i)
      return i + 1
    end
  end
</script>
<button _="on click call Incrementer.increment(41) then put 'The answer is: ' + it into my.innerHTML">
  Call a Worker...
</button>

This makes it very easy to define and work with web workers.

Note that you can use the inline js feature discussed next if you want to use JavaScript in your worker. You might want to do this if you need better performance on calculations than hyperscript provides, for example.

Web Sockets

Web Sockets allow for two-way communication with a web server, and are becoming increasingly popular for building web applications. Hyperscript provides a simple way to create them, as well as a simple Remote Procedure Call (RPC) mechanism layered on top of them, by using the socket keyword.

Here is a simple web socket declaration in hyperscript:

socket MySocket ws://myserver.com/example
  on message as json
    log message
end

This socket will log all messages that it receives as a parsed JSON object.

You can send messages to the socket by using the normal send command:

send myMessage(foo: "bar", doh: 42) to MySocket

You can read more about the RPC mechanism on the socket page.

Event Source

Server Sent Events are a simple way for your web server to push information directly to your clients that is supported by all modern browsers.

They provide real-time, uni-directional communication from your server to a browser. Server Sent Events cannot send information back to your server. If you need two-way communication, consider using sockets instead.

You can declare an SSE connection by using the eventsource keyword and can dynamically change the connected URL at any time without having to reconnect event listeners.

Here is an example event source in hyperscript:

eventsource ChatUpdates from http://myserver.com/chat-updates

  on message as string
    put it into #div
  end

  on open
    log "connection opened."
  end

end

This event source will put all message events in to the #div and will log when an open event occurs. This feature also publishes events, too, so you can listen for Server Sent Events from other parts of your code.

Debugging

Debugging hyperscript can be done a few different ways. The simplest and most familiar way for most developers to debug hyperscript is the use of the log command to log intermediate results. This is the venerable "print debugging":

get <div.highlighted/> then log the result

This is a reasonable way to start debugging but it is, obviously, fairly primitive.

Beeping

An annoying aspect of print debugging is that you are often required to extract bits of expressions in order to print them out, which can interrupt the flow of code. Consider this example of hyperscript:

  add .highlighted to <p/> in <div.hilight/>

If this wasn't behaving as you expect and you wanted to debug the results, you might break it up like so:

  set highlightDiv to <div.hilight/>
  log highlightDiv
  set highlightParagraphs to <p/> in highlightDiv
  log highlightParagraphs
  add .highlighted to highlightParagraphs

This is a fairly violent code change and it obscures what the actual logic is.

To avoid this, hyperscript offers a beep! operator. The beep! operator can be thought of as a pass-through expression: it simply passes the value of whatever expression comes afterwards through unmodified.

However, along the way, it logs the following information to the console:

So, considering the above code, rather than breaking things up, we might just try this:

  add .highlighted to <p/> in beep! <div.hilight/>

Here we have added a beep! just before the <div.hilight/> expression. Now when the code runs we will see the following in the console:

///_ BEEP! The expression (<div.hilight/>) evaluates to: [div.hilight] of type ElementCollection

You can see the expressions source, its value (which you can right click on and assign to a temporary value to work with in most browsers) as well as the type of the value. All of this had no effect on the evaluation of the expression or statement.

Let's store the ElementCollection as a temporary value, temp1.

We could now move the beep! out to the in expression like so:

  add .highlighted to beep! <p/> in <div.hilight/>

And we might see results like this:

///_ BEEP! The expression (<p/> in <div.hilight/>) evaluates to: [] of type Array

Seeing this, we realize that no paragraphs elements are being returned by the in expression, which is why the class is not being added to them.

In the console we check the length of the original ElementCollection:

> temp1.length
0

And, sure enough, the length is zero. On inspecting the divs in question, it turns out we had misspelled the class name hilight rather than highlight.

After making the fix, we can remove the beep! (which is obviously not supposed to be there!):

  add .highlighted to <p/> in <div.highlight/>

And things work as expected.

As you can see, beep! allows us to do much more sophisticated print debugging, while not disrupting code nearly as drastically as traditional print debugging would require.

You can also use beep! as a command to assist in your debugging.

HDB - The Hyperscript Debugger

An even more sophisticated debugging technique is to use hdb, the Hyperscript Debugger, which allows you to debug by inserting breakpoint commands in your hyperscript.

Note: The hyperscript debugger is in alpha and, like the rest of the language, is undergoing active development

To use it you need to include the lib/hdb.js file. You can then add breakpoint commands in your hyperscript to trigger the debugger.

Example: Debugging
<button _="
on click
tell next <output/>
  breakpoint
  put 'You can click <kbd><samp>Step Over</samp></kbd> to execute the command' into you
  put 'Click the <kbd><samp>&rdca;</kbd></samp> button to skip to a command'   into you
  put 'Click <kbd><samp>Continue</samp></kbd> when you’re done'                into you
  put '--'                                                                     into you
">Debug</button>
<output>--</output>

--

Extending

Hyperscript has a pluggable grammar that allows you to define new features, commands and certain types of expressions.

Here is an example that adds a new command, foo, that logs `"A Wild Foo Was Found!" if the value of its expression was "foo":

// register for the command keyword "foo"
_hyperscript.addCommand('foo', function(parser, runtime, tokens) {

  // A foo command  must start with "foo".
  if(!tokens.match('foo')) return

  // Parse an expression.
  const expr = parser.requireElement('expression', tokens);

  return {
    // All expressions needed by the command to execute.
    // These will be evaluated and the result will be passed back to us.
    args: [expr],

    // Implement the logic of the command.
    // Can be synchronous or asynchronous.
    // @param {Context} context The runtime context, contains local variables.
    // @param {*} value The result of evaluating expr.
    async op(context, value) {
      if (value == "foo") {
        console.log("A Wild Foo Was Found!")
      }
      // Return the next command to execute.
      return runtime.findNext(this)
    }
  }
})

With this command defined you can now write the following hyperscript:

  def testFoo()
    set str to "foo"
    foo str
  end

And "A Wild Foo Was Found!" would be printed to the console.

Security

Hyperscript allows you to define logic directly in your DOM. This has a number of advantages, the largest being Locality of Behavior making your system more coherent.

One concern with this approach, however, is security. This is especially the case if you are injecting user-created content into your site without any sort of HTML escaping discipline.

You should, of course, escape all 3rd party untrusted content that is injected into your site to prevent, among other issues, XSS attacks. The _, script and data-script attributes, as well as inline <script> tags should all be filtered.

Note that it is important to understand that hyperscript is interpreted and, thus, does not use eval (except for the inline js features). You (or your security team) may use a CSP that disallows inline scripting. This will have no effect on hyperscript functionality, and is almost certainly not what you (or your security team) intends.

To address this, if you don't want a particular part of the DOM to allow for hyperscript interpretation, you may place a disable-scripting or data-disable-scripting attribute on the enclosing element of that area.

This will prevent hyperscript from executing within that area in the DOM:

  <div data-disable-scripting>
    <%= user_content %>
  </div>

This approach allows you enjoy the benefits of Locality of Behavior while still providing additional safety if your HTML-escaping discipline fails.

Language History

The initial motivation for hyperscript came when I ported intercooler.js to htmx. Intercooler had a feature, ic-action that allowed for some simple client-side interactions. One of my goals with htmx was to remove non-core functionality from intercooler, and really focus it in on the hypermedia-exchange concept, so ic-action didn't make the cut.

However, I couldn't shake the feeling that there was something there: an embedded, scripty way of doing light front end coding. It even had some proto-async transparent features. But, with my focus on htmx, I had to set it aside.

As I developed htmx, I included an extensive event model. Over time, I realized that I wanted to have a clean way to utilize these events naturally and directly within HTML. HTML supports on* attributes for handling standard DOM events (e.g. onClick) of course, but they don't work for custom events like htmx:load.

The more I looked at it, the more I thought that there was a need for a small, domain specific language that was event oriented and made DOM scripting efficient and fun. I had programmed in HyperTalk, the scripting language for HyperCard, when I was younger and remembered that it integrated events very well into the language. So I dug up some old documentation on it and began work on hyperscript, a HyperTalk-derived scripting language for the web.

And here we are. I hope you find the language useful, or, at least, funny. :)