We are pleased to present the 0.9.90 release of hyperscript. This is a significant release that includes a complete internal restructuring, a new reactivity system, many new commands and expressions, and improved error handling.
The headline feature of this release is a new reactivity system with three features that let you declare relationships between values and have them stay in sync automatically.
Keeps the DOM in sync with values. Each command in an always block becomes an independent
tracked effect that re-runs when its dependencies change:
<button _="on click increment $count">+1</button>
<output _="always put 'Count: ' + $count into me"></output>
when reacts to value changes with side effects:
<div _="when $source changes set $derived to (it * 2)"></div>
<output _="when $derived changes put it into me"></output>
bind keeps two values in sync (two-way binding). On form elements it auto-detects the right
property (value, checked, valueAsNumber):
<input type="checkbox" id="dark-toggle" />
<body _="bind .dark and #dark-toggle's checked">
The reactivity system includes automatic dependency tracking, circular dependency detection, and cleanup when elements are removed from the DOM.
Open and close dialogs, details elements, popovers, and fullscreen. The command automatically detects the element type and calls the right API:
open #my-dialog -- showModal() for <dialog>
close #my-dialog -- close() for <dialog>
open #my-details -- sets open on <details>
open fullscreen -- fullscreen the entire page
open fullscreen #video -- fullscreen a specific element
close fullscreen -- exit fullscreen
focus and blur set or remove keyboard focus. Default to me if no
target is given:
on click focus #name-input
on submit blur me
empty removes all children from an element:
on click empty #results
Select the text content of an input or textarea:
on focus select #search-input
Access the browser's built-in dialogs. ask wraps prompt() and places the result in
it. answer wraps alert(), or confirm() when given two choices:
ask "What is your name?"
put it into #greeting
answer "File saved!"
answer "Save changes?" with "Yes" or "No"
if it is "Yes" ...
Text-to-speech via the Web Speech API — a nod to HyperTalk. The command waits for the utterance to finish before continuing:
speak "Hello world"
speak "Quickly now" with rate 2 with pitch 1.5
scroll scrolls elements into view with alignment, offset, and smooth scrolling:
scroll to #target
scroll to the top of #target smoothly
scroll to the bottom of me +50px
Pause execution in the browser DevTools. Now built in to core — no hdb extension required:
on click
breakpoint
add .active
Filter, sort, map, split, and join collections with postfix expressions that chain naturally.
it/its refer to the current element:
items where its active sorted by its name mapped to its id
"banana,apple,cherry" split by "," sorted by it joined by ", "
<li/> in #list where it matches .visible
New magic symbols for accessing the system clipboard and current text selection:
put clipboard into #paste-target -- async read, auto-awaited
set clipboard to "copied!" -- sync write
put selection into #selected-text -- window.getSelection().toString()
ResizeObserver as a synthetic event,
matching the pattern of on mutation and on intersection:
on resize put `${detail.width}x${detail.height}` into #size
on resize from #panel put detail.width into me
One-shot event handlers that fire only once:
on first click add .loaded to me then fetch /data
Case-insensitive modifier for comparisons:
if my value contains "hello" ignoring case ...
show <li/> when its textContent contains query ignoring case
Apply commands conditionally per element. After execution, the result contains the matched elements.
Works on add, remove, show, and
hide:
show <li/> in #results when its textContent contains my value
show #no-match when the result is empty
Loops that run the body at least once before checking the condition:
repeat
increment x
until x is 10 end
Chain conversions left to right:
get #myForm as Values | JSONString
get #myForm as Values | FormEncoded
Variables with the ^ prefix are scoped to the element and inherited by all descendants,
ideal for component state without polluting the global scope:
<div _="init set ^count to 0">
<button _="on click increment ^count">+1</button>
<output _="always put ^count into me"></output>
</div>
transition and measure --
the internal pseudopossessive parsing mechanism has been removed; these commands now use the same expression
syntax as everything elseThis release is a complete ESM rewrite of the codebase (45 modules). Element state is now stored on
elt._hyperscript (inspectable in DevTools), aligned with htmx's elt._htmx pattern. The test suite was
migrated to Playwright. See the CHANGELOG for the full list of internal changes and bug fixes.
If you are upgrading from 0.9.14 or earlier, the following breaking changes may require updates to your code.
All extension scripts have been reorganized into a dist/ext/ subdirectory.
Upgrade Step: Search for dist/hdb.js, dist/socket.js, dist/worker.js, dist/eventsource.js,
dist/template.js, dist/tailwind.js and replace with dist/ext/hdb.js, dist/ext/socket.js, etc.
The transition command previously accepted bare identifiers like width and opacity
as CSS property names. Now that hyperscript has style literals (*width, *opacity),
transition requires them for consistency with the rest of the language. The element keyword prefix for
targeting other elements has also been removed in favor of standard possessive and
of syntax.
-- Before -- After
transition width to 100px transition *width to 100px
transition my opacity to 0 transition my *opacity to 0
transition element #foo width to 100px transition #foo's *width to 100px
transition now also supports of syntax: transition *opacity of #el to 0.
Upgrade Step: Search for transition commands and add * before style property names. Replace
transition element #foo with transition #foo's.
In previous versions, as JSON called JSON.stringify(). It now calls JSON.parse(),
matching the natural reading of "interpret this as JSON". A new as JSONString conversion handles
stringification.
Upgrade Step: Search for as JSON. If it was being used to stringify an object, replace with as JSONString.
If it was parsing a JSON string, no change needed.
The colon-based conversion modifiers have been replaced by the more general pipe operator.
Upgrade Step: Replace as Values:JSON with as Values | JSONString. Replace as Values:Form with
as Values | FormEncoded.
default previously used a truthy check, so default x to 10 would overwrite 0 and
false. It now uses a nullish+empty check: only null, undefined, and "" are considered unset.
Upgrade Step: If you relied on default overwriting falsy values like 0 or false, use an explicit
if instead.
The [@attr] bracket-style attribute access has been deprecated in favor of the
@attr literal.
Upgrade Step: Replace [@attr] with @attr.
The url keyword in go to url X is no longer needed — go to now accepts naked URLs
directly.
The scroll form go to the top of ... continues to work but has been superseded by the dedicated
scroll command.
Upgrade Step: Replace go to url X with go to X. If you like, replace go to the top of #el with
scroll to the top of #el.
The API has been renamed to process() to align with htmx's naming. The old name still works as an alias.
Upgrade Step: Replace _hyperscript.processNode( with _hyperscript.process(.
dist/_hyperscript.js is now IIFE (was UMD). Plain <script> tags work unchanged.
Upgrade Step: If you use ES module imports, switch to dist/_hyperscript.esm.js.
Enjoy!