mizu.js

Supercharges your HTML with ease.

Features

Plug-and-play

Simply include the library and start building amazing things instantly with vanilla JavaScript expressions and HTML.

Cross-platform

Compatible across a wide range of JavaScript and TypeScript runtimes, including all major browsers.

Any-side

Render your content wherever you need it and however you want it with user-friendly APIs.

Hackable

Cherry-pick features and craft your own setup easily with developer-friendly APIs and our custom builder.

Community-driven

Build, share and reuse custom elements and directives to supercharge your development.

Want to effortlessly theme your page? Check out matcha.css!

Open-core

Licensed under the AGPLv3 License, source code fully available on github.com.

Use MIT License terms for non-commercial projects or with an active $1+ monthly sponsorship.
File
Edit
View
Run
Terminal
Help
ssr.ts
ssg.ts
ssr.mjs
ssg.mjs
ssr.ts
ssg.ts
#!/usr/bin/env -S deno serve --allow-read --allow-env
// Server-Side Rendering (SSR) with Mizu
import Mizu from "@mizu/render/server"

export default {
  async fetch() {
    const headers = new Headers({ "Content-Type": "text/html; charset=utf-8" })
    const body = await Mizu.render(`<div *text="foo"></div>`, { context: { foo: "🌊 Yaa, mizu!" } })
    return new Response(body, { headers })
  },
}
#!/usr/bin/env -S deno run --allow-read --allow-env --allow-net --allow-write=/tmp/output
// Static Site Generation (SSG) with Mizu
import Mizu from "@mizu/render/server"

await Mizu.generate([
  // Copy content from strings
  [`<div *text="foo"></div>`, "index.html", { render: { context: { foo: "🌊 Yaa, mizu!" } } }],
  // Copy content from callback return
  [() => JSON.stringify(Date.now()), "timestamp.json"],
  // Copy content from local files
  ["**/*", "static", { directory: "/fake/path" }],
  // Copy content from URL
  [new URL("https://matcha.mizu.sh/matcha.css"), "styles.css"],
], { clean: true, output: "/tmp/output" })
1
2
3
4
5
6
7
8
9
10
11
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Server-Side Rendering (SSR) with Mizu
import Mizu from "@mizu/render/server"
import { createServer } from "node:http"

createServer(async (_, response) => {
  response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" })
  response.end(await Mizu.render(`<div *text="foo"></div>`, { context: { foo: "🌊 Yaa, mizu!" } }))
}).listen(8000, "0.0.0.0", () => console.log("Server is listening"))
// Static Site Generation (SSG) with Mizu
import Mizu from "@mizu/render/server"

await Mizu.generate([
  // Copy content from strings
  [`<div *text="foo"></div>`, "index.html", { render: { context: { foo: "🌊 Yaa, mizu!" } } }],
  // Copy content from callback return
  [() => JSON.stringify(Date.now()), "timestamp.json"],
  // Copy content from local files
  ["**/*", "static", { directory: "/fake/path" }],
  // Copy content from URL
  [new URL("https://matcha.mizu.sh/matcha.css"), "styles.css"],
], { clean: true, output: "/tmp/output" })
1
2
3
4
5
6
7
8
1
2
3
4
5
6
7
8
9
10
11
12
13
// Server-Side Rendering (SSR) with Mizu
import Mizu from "@mizu/render/server"

Bun.serve({
  port: 8000,
  async fetch() {
    const headers = new Headers({ "Content-Type": "text/html; charset=utf-8" })
    const body = await Mizu.render(`<div *text="foo"></div>`, { context: { foo: "🌊 Yaa, mizu!" } })
    return new Response(body, { headers })
  },
})

console.log("Server is listening")
// Static Site Generation (SSG) with Mizu
import Mizu from "@mizu/render/server"

await Mizu.generate([
  // Copy content from strings
  [`<div *text="foo"></div>`, "index.html", { render: { context: { foo: "🌊 Yaa, mizu!" } } }],
  // Copy content from callback return
  [() => JSON.stringify(Date.now()), "timestamp.json"],
  // Copy content from local files
  ["**/*", "static", { directory: "/fake/path" }],
  // Copy content from URL
  [new URL("https://matcha.mizu.sh/matcha.css"), "styles.css"],
], { clean: true, output: "/tmp/output" })
1
2
3
4
5
6
7
8
9
10
11
12
13
1
2
3
4
5
6
7
8
9
10
11
12
13
Terminal
Output
Ports
dev@mizu:~ $
deno add jsr:@mizu/render
npx jsr add @mizu/render
bunx jsr add @mizu/render
IIFE
ESM
🌊 Yaa, mizu!
Elements
<!DOCTYPE html>
<html>
  <head>
    <title>IIFE</title>
    <meta charset="UTF-8" />
  </head>
  <body>
    <main *mizu *set="{ foo: '🌊 Yaa, mizu!' }">
      <div *text="foo"></div>
    </main>
    <!-- Use the IIFE version to automatically start mizu -->
    <script src="https://mizu.sh/client.js" defer></script>
  </body>
</html>
<!DOCTYPE html>
<html>
  <head>
    <title>ESM</title>
    <meta charset="UTF-8" />
  </head>
  <body>
    <main *mizu>
      <div *text="foo"></div>
    </main>
    <!-- Use the ESM version to manually control mizu -->
    <script type="module">
      import Mizu from "https://mizu.sh/client.mjs"
      await Mizu.render(document.body, { context: { foo: "🌊 Yaa, mizu!" } })
    </script>
  </body>
</html>
browsers
deno
node
bun

Want to see mizu.js in action?

Dive into our interactive playground and start experimenting!

FAQ

Another web framework? Really? In 2024?

Yes, but hear us out!

Remember when building a web page was as simple as writing some HTML, adding a bit of JavaScript, and styling with CSS? mizu.js brings back that simplicity, offering a modern yet flexible approach to web development.

By adhering closely to web standards and embracing the simplicity of plain HTML and JavaScript, mizu.js offers an almost non-existent learning curve. This makes it an excellent choice for both beginners and seasoned developers.

Whether you're serving content from your favorite runtime, generating static websites, or creating dynamic pages in the browser, mizu.js adapts to your needs. — All of it without the hassle of bundlers, transpilers, or countless dependencies.

Why should I use mizu.js over other web frameworks?

We recognize the power and utility of comprehensive frameworks like React, Vue.js, and Angular. These tools excel at building complex applications and boast large communities and ecosystems. However, they can be overwhelming for smaller projects, often requiring a build step and a steep learning curve with their specific syntax and concepts.

Lightweight alternatives such as Alpine.js and htmx are also available. While they perform well in the browser, they were often not designed for server-side use, limiting their applicability across different scenarios.

mizu.js draws inspiration from all these frameworks but is designed from the ground up to be:

  • Flexible, suitable for single page applications, static websites, interactive websites, and more.
  • Versatile, works both client-side and server-side and in the runtime of your choice.
  • Simple, close to vanilla JavaScript and HTML and a minimal learning curve.
  • Customizable, easily opt-in/opt-out of features.

If all of this sounds appealing to you, then mizu.js might be the right choice for your next project.

Does mizu.js use a Virtual DOM?

It depends.

On the server-side, mizu.js employs a Virtual DOM to simulate the browser environment. The current implementation leverages JSDOM, but you can use any other Virtual DOM library that adheres to web standards. Non-compliant implementations may lead to unexpected behaviors.

On the client-side, mizu.js interacts directly with the actual DOM. It tracks processed elements and their states using weak references to prevent memory leaks and enhance performance.

How reliable is mizu.js?

We want to ensure mizu.js delivers a reliable and consistent experience across all supported environments, which is why each feature is meticulously documented to detail its behavior and functionality.

Our codebase is thoroughly tested and covered to guarantee that each feature performs as expected.

If you encounter any issues or undocumented behavior, please open a new issue so we can address it promptly.

What does the name mizu mean?

æ°´(mizu) is the Japanese kanji for water.

Like water, mizu.js is fluid and adaptable, seamlessly fitting into various use cases and execution environments.

Like water, mizu.js is simple and fundamental, staying close to vanilla JavaScript and HTML with a minimal learning curve.

Like water, mizu.js is customizable, it can be mixed with other libraries and additional features to suit your needs.

Need to add some flavor to your page? Add some matcha.css to your mizu.js project!
What is the licensing model of mizu.js?

mizu.js is licensed under the AGPLv3 License, allowing you to use it freely for any project, provided you also distribute the source code of your project that relies on it.

Alternatively, you can use mizu.js under the MIT License instead if you meet one of the following conditions:

In summary, mizu.js is free for open-source and non-commercial projects, while a small contribution is required for commercial closed-source projects to support its development.

Usage

Client-side

Set up mizu.js in your browser environment using one of two methods:

On the client-side...

  • Rendering is explicit, requiring the *mizu attribute to enable mizu.js on a subtree.
  • Reactivity is enabled, so changes to contexts will trigger a re-render.

IIFE (.js)

This setup automatically starts rendering the page once the script is loaded. It's the simplest way to get started but limited to the default configuration.

<script src="https://mizu.sh/client.js" defer></script>

ESM (.mjs)

This setup requires you to import and start mizu.js manually, allowing customization of the rendering process, such as setting the initial context and loading additional directives.

<script type="module">
  import Mizu from "https://mizu.sh/client.mjs"
  await Mizu.render(document.body, { context: { foo: "🌊 Yaa, mizu!" } })
</script>

Server-side

To set up mizu.js in a server environment, install it locally. mizu.js packages are hosted on jsr.io/@mizu.

On the server side...

  • Rendering is implicit, so the *mizu attribute is not required.
  • Reactivity is disabled, meaning changes to contexts are not tracked and will not trigger a re-render.

Deno

Deno supports the jsr: specifier natively, allowing you to import mizu.js directly.

import Mizu from "jsr:@mizu/render/server"
await Mizu.render(`<div *text="foo"></div>`, { context: { foo: "🌊 Yaa, mizu!" } })

Alternatively, add it to your project using the Deno CLI.

deno add jsr:@mizu/render

Other runtimes (NodeJS, Bun, etc.)

Add mizu.js to your project using the JSR npm compatibility layer.

# NodeJS
npx jsr add @mizu/render
# Bun
bunx jsr add @mizu/render

Once installed, use it in your project.

import Mizu from "@mizu/render/server"
await Mizu.render(`<div *text="foo"></div>`, { context: { foo: "🌊 Yaa, mizu!" } })

Concepts

Directive

A HTML attribute recognized by mizu.js which instructs how it should process the element.

The syntax is as follows:

<tag 
*name
[tag]
.modifier
[value]
="expression"
 />

Name

Directives names often begin with special characters to prevent conflicts with standard HTML attributes and to clearly indicate their specific purpose:

  • * for generic directives.
  • # for directives targeting <slot> elements.
  • @ for directives related to Event handling.
  • : for binding HTML attributes.
    • :: for bi-directional binding.
  • % for HTTP directives.
    • %@ for combined HTTP and Event directives.
  • ~ for testing directives.

Tag

Typically, this serves as the directive's argument. At most one tag can be specified per directive.

Modifier(s)

Modifiers adjust the behavior of the directive. You can specify multiple modifiers on the same directive.

Repeating a modifier on a single directive is considered undefined behavior.

Modifier(s) value

The value of the modifier. The modifier may have an explicit default value, which is used if no value is specified.

The value is validated and cast to the expected type, defaulting if invalid. Modifiers may accept one of the following types:

  • boolean , unset defaults to true.
  • string , unset defaults to "".
  • number , unset defaults to 0.
  • duration , unset defaults to 0ms (supported units: ms, s, m).

JavaScript Expression

Set through the HTML attribute value, it is evaluated asynchronously within the context of the current element during processing.

A directive may have a Default expression, used when no specific expression is provided.

Unless stated otherwise, any JavaScript expression is permitted.

The full capabilities of the JavaScript engine is available, including its potential risks. Exercise caution when using expressions from untrusted sources.

Context

All expressions are evaluated within a specific context, which includes all defined variables and functions. You can define these through the user API or directly via contextual directives.

Certain directives may also provide special variables based on the current processing state, prefixed with a dollar sign ($).

Although you can also prefix your variables with a dollar sign ($), it is recommended to avoid this practice to prevent confusion with special variables. In case of a conflict, the special variables will shadow user-defined ones until being shadowed themselves.
The identifier __mizu_internal is reserved and cannot be used in expressions.

Reactivity

mizu.js leverages @libs/reactive to monitor context changes and trigger re-renders as needed.

During element processing, all get operations are cached with the element as a reference. Whenever a set operation is performed, mizu.js identifies which elements depend on the changed value and re-processes them.

Reactivity is enabled by default on the Client-side but disabled by default on the Server-side.
Although @libs/reactive can track function calls, mizu.js currently does not support reprocessing elements that depend on them. This is because expressions are re-evaluated with each re-render, which would re-trigger the function call. Future versions of mizu.js may introduce a mechanism to prevent this behavior.

Rendering

Rendering is the process of evaluating and applying mizu.js directives. This occurs recursively on a subtree and modifies the current DOM.

Rendering process schema explanation
Element rendering

Node is an Element OR Node is in cache

Resolve source node IF in cache

Directives setup

Returned value is false

Call directive.setup()

Update state IF present in returned value

Set forced eligibility IF present in returned value

Directives execution

Node has a matching attribute OR eligibility has been forced to true during setup

Node has NO matching attributes OR eligibility has been forced to false during setup

Returned value has final set to true

Call directive.execute()

Notify any misuses IF applicable (e.g. conflicts, duplicates, etc.)

Replace current node reference IF present in returned value

Update state IF present in returned value

Update context IF present in returned value

Recursively call rendering on node children

Directives cleanup

Call directive.cleanup()

Dynamically adding and removing mizu.js directives is not officially supported. While it may work currently due to the rendering engine's lack of optimization, future versions may cache directives to enhance performance. This change would prevent dynamically added directives from being processed.

Phase

Each directive is tied to a specific phase, dictating the order of processing. The sequence in which directives appear on an element is irrelevant unless they share the same phase.

Directives within the same phase cannot coexist on the same element unless explicitly allowed to be specified Multiple times.

Using conflicting directives on the same element results is considered undefined behavior.
Phase list
  • 00 META
  • 01 ELIGIBILITY
  • 02 PREPROCESSING
  • 10 TESTING
  • 11 CONTEXT
  • 21 EXPAND
  • 22 MORPHING
  • 23 TOGGLE
  • 31 HTTP_HEADER
  • 32 HTTP_BODY
  • 33 HTTP_REQUEST
  • 34 HTTP_CONTENT
  • 35 HTTP_INTERACTIVITY
  • 41 CONTENT
  • 42 CONTENT_INTERPOLATION
  • 49 CONTENT_CLEANING
  • 51 ATTRIBUTE
  • 52 ATTRIBUTE_MODEL_VALUE
  • 59 ATTRIBUTE_CLEANING
  • 61 INTERACTIVITY
  • 71 DISPLAY
  • 81 CUSTOM_ELEMENT
  • 82 REFERENCE
  • 89 CUSTOM_PROCESSING
  • 99 POSTPROCESSING

Directives

Rendering

*mizu
Phase1 — ELIGIBILITY

Enable mizu.js rendering for the element and its children.

<main *mizu>
  <!--...-->
</main>
1 Restriction 1 Note $root
For performance reasons, this directive must not have any [tag] or .modifiers. If it does, the directive will be ignored.
You can choose whether to require this directive for mizu.js rendering with the implicit option when using the user API. By default, rendering is explicit in Client-Side APIs and implicit in Server-Side APIs.

Variables

$rootHTMLElement
The closest element that declares a *mizu directive.

Contextual

*set="context"
Phase11 — CONTEXT

Set context values for an element and its children.

<div *set="{ foo: 'bar' }">
  <!--<span *text="foo"></span>-->
</div>
1 Restriction 1 Note
The context must resolve to a JavaScript Object.
The context is initialized once and persists across renderings, but it can still be updated by other directives.

*ref="name"
Phase82 — REFERENCE

Create a reference to an element for later use.

<div *ref="foo" data-text="bar">
  <!--<p *text="$refs.foo.dataset.text"></p>-->
</div>
1 Note $refs .raw
Redefining a reference will shadow its previous value within the current subtree, without affecting its value in the parent subtree.

Variables

$refsRecord<PropertyKey, HTMLElement>
A collection of all referenced elements within the current subtree.

Modifiers

.raw[boolean=true]
Skip expression evaluation if set.

Conditional

*if="expression"
Phase23 — TOGGLE

Conditionally render an element.

<div *if="true">
  <!--...-->
</div>
1 TBD
There is currently no special handling for <template> elements, but future versions may introduce specific behavior for these elements.

*else="expression"
Defaulttrue Phase23 — TOGGLE

Conditionally render an element placed after another *if or *else directive.

<div *if="false"></div>
<div *else="false"></div>
<div *else><!--...--></div>
1 Restriction 1 TBD
Must be placed immediately after an element with an *if or *else directive.
There is currently no special handling for <template> elements, but future versions may introduce specific behavior for these elements.

*show="expression"
Defaulttrue Phase71 — DISPLAY

Conditionally display an element.

<div *show="true">
  <!--...-->
</div>
4 Notes
When hidden, the element's CSS display property is set to none !important.
When shown and if initially hidden by a CSS stylesheet (display: none), the element's display property is reset to initial !important.
Unlike *if and *else directives, the element remains in the DOM when hidden.
You can take advantage of the default value being true to hide elements before mizu.js loads (e.g. <style>[\*show]{display:none}</style>).

Iterative

*for="expression"
Phase21 — EXPAND

Render an element for each iteration performed.

<!--<ul>-->
<li *for="let item of items"></li>
<!--</ul>-->
1 Restriction 2 TBDs $id $iterations $i $I $first $last
The expression can be:
  • Any syntax supported inside for, for...in and for...of loops.
  • Any iterable object that implements Symbol.iterator.
    • The iterated key is exposed as $key.
    • The iterated value is exposed as $value.
  • A finite number.
    • The directive is applied the specified number of times.
There is currently no distinction between let, const and var declarations inside for loops, but future versions may introduce specific behavior for these.
There is currently no special handling for <template> elements, but future versions may introduce specific behavior for these elements.

Variables

$idstring
The evaluated value of the *id directive if present, or the auto-generated identifier.
$iterationsnumber
The total number of iterations.
$inumber
The current iteration index (0-based).
$Inumber
The current iteration index (1-based, same as $i + 1).
$firstnumber
Whether this is the first iteration (same as $i === 0).
$lastnumber
Whether this is the last iteration (same as $i === ($iterations - 1)).

*id="expression"
Phase0 — META

Hint for *for directive to differentiate generated elements.

<!--<ol>-->
<li *for="const {id} of items" *id="id"></li>
<!--</ol>-->
1 Restriction 1 Note
Must be used on an element with a *for directive.
Identifiers must be unique within the loop, any duplicates will be replaced by the last occurrence.

*empty
Phase23 — TOGGLE

Conditionally render an element after a *for directive.

<article *for="const article of articles"></article>
<p *empty.not *text="`${$generated} results`"></p>
<p *empty><!-- No results.--></p>
1 Restriction 1 TBD $generated .not
Must be placed immediately after an element with a *for or another *empty directive. Elements generated by the *for directive do not apply to this restriction.
There is currently no special handling for <template> elements, but future versions may introduce specific behavior for these elements.

Variables

$generatednumber
The number of elements generated by the preceding *for directive. This value may differ from the actual number of iterations processed if conditional directives were applied.

Modifiers

.not[boolean]
Inverts the condition, rendering the element when at least one element is generated.

Content

*text="content"
Defaultthis.innerHTML Phase41 — CONTENT

Set element's textContent.

<p *text="'...'">
  <!--...-->
</p>
2 Notes
HTML content is automatically escaped.
Without an attribute value, this directive escapes the element's innerHTML (e.g., <a *text><b></b></a> becomes <a *text>&lt;b&gt;&lt;/b&gt;</a>).

*html="content"
Phase41 — CONTENT

Set element's innerHTML.

<template *html="'<p>...</p>'">
  <!--<p>...</p>-->
</template>
1 Warning
Raw HTML can introduce XSS vulnerabilities. Exercise caution when using expressions from untrusted sources.

*mustache
Multiple Phase42 — CONTENT_INTERPOLATION

Enable content interpolation within mustaches ({{ and }}) from Text child nodes.

<p *mustache>
  <!--{{ ... }}-->
</p>
2 Notes 1 TBD
Interpolation occurs only within Text nodes, not the entire element.
HTML content is automatically escaped.
There is currently no distinction between double mustaches ({{ and }}) and triple mustaches ({{{ and }}}), but future versions may introduce specific behavior for these.

*code="content"
Defaultthis.textContent Phase41 — CONTENT

Set element's innerHTML after performing syntax highlighting.

<code *code[ts]="'...'">
  <!--<span class="hljs-*">...</span>-->
</code>
1 Import 1 Note [tag] .trim
This directive dynamically imports highlight.js.
Unsupported languages default to plaintext.

Modifiers

[string]
Any supported language identifier or alias.
.trim[boolean=true]
Remove leading/trailing whitespaces and shared indentation.

*markdown="content"
Defaultthis.textContent Phase41 — CONTENT

Set element's innerHTML after performing markdown rendering.

<div *markdown="'*...*'">
  <!--<em>...</em>-->
</div>
1 Import [tag]
This directive dynamically imports @libs/markdown.

Modifiers

[string]
Load additional Markdown plugins by specifying a comma-separated list (e.g., *markdown[emojis,highlighting,sanitize]). See the full list of supported plugins at @libs/markdown/plugins. Unsupported plugins will be silently ignored.

*toc="selector"
Default'main' Phase41 — CONTENT

Create a table of contents from <h1>...<h6> elements found in selected target.

<nav *toc="'main'">
  <!--<ul>...</ul>-->
</nav>
1 Restriction 1 Note [tag]
Heading elements must meet these criteria:
  • Include an id attribute.
  • Contain an immediate <a> child with an anchor link pointing to its parent id.
  • Follow a descending order without skipping levels.
When a heading is found, the next level headings are searched within its parentElement. If the parent is an <hgroup> or has a *toc[ignore] attribute, the search moves to the grandparent element.

Modifiers

[string]
Define which heading levels to include:
  • Specify a single level (e.g., *toc[h2]).
    • Add a + to include higher levels (e.g., *toc[h2+]).
  • Use a range with a - to specify multiple levels (e.g., *toc[h2-h4]).
  • Use ignore to exclude an element from traversal (e.g., *toc[ignore]). No other modifiers or attribute value should be used with this.

*clean
Phase49 — CONTENT_CLEANING

Clean up the element and its children from specified content.

<div *clean>
  <!--...-->
</div>
.comments .spaces .templates .directives

Modifiers

.comments[boolean]
Remove all Comment nodes within the subtree.
.spaces[boolean]
Remove all spaces (except non-breaking spaces &nbsp;) within the subtree.
.templates[boolean]
Clear all <template> nodes from the subtree after fully processing it.
.directives[boolean]
Strip all known directives from the subtree after fully processing it. If the .comments modifier is also enabled, comments generated by directives will be removed as well.

Custom elements

*custom-element="tagname"
Phase81 — CUSTOM_ELEMENT

Register a new custom element.

<template *custom-element="my-element">
  <ul><slot name="items"></slot></ul>
</template>
2 Restrictions 2 Notes $slots $attrs .flat
Must be defined on a <template> element.
The tagname must be a valid custom element name.
Valid custom element names may be specified as is.
Custom elements registered this way do not use Shadow DOM, their content is rendered directly.

Variables

$slotsRecord<PropertyKey, HTMLSlotElement>
A record of #slot elements by <slot> name. The unnamed slot is accessible using $slots[""].
$attrsRecord<PropertyKey, string>
A record of HTML attributes specified on the custom element.

Modifiers

.flat[boolean]
Replace occurrences of this custom element with its content. Note that $slots and $attrs variables are not accessible when using this modifier.

#slot
Phase0 — META

Specify target <slot> in an element defined by a *custom-element directive.

<my-element>
  <li #items><!--...---></li>
</my-element>
2 Notes
Elements without a #slot directive are appended to the default (unnamed) slot.
Elements targeting the same slot are appended in the order they are defined.

*is="tagname"
Phase22 — MORPHING

Set an element tagname.

<div *is="'section'">
  <!--...-->
</div>
1 Warning
If the tagname changes, the reference will also change. Equality checks with elements using this directive may not work as expected. Some directives may be incompatible with this directive.

Events

@event="listener"
Defaultnull Multiple Phase61 — INTERACTIVITY

Listen for a dispatched Event.

<button @click="this.value = 'Clicked!'">
  <!--Not clicked yet.-->
</button>
2 Notes $event [tag] .prevent .stop .once .passive .capture .self .attach .throttle .debounce .keys
Attach multiple listeners in a single directive using the shorthand @="object" (e.g., @="{ foo() {}, bar() {} }").
  • Modifiers apply to all listeners in the directive (e.g., @.prevent="{}").
  • Use tags to attach listeners with different modifiers (e.g., @[1]="{}" @[1].prevent="{}").
  • HTML attributes are case-insensitive, so this is the only way to listen for events with uppercase letters or illegal attribute characters (e.g., @="{ FooBar() {}, Foobar() {} }").
To listen for events with dots . in their names, use brackets {} (e.g. @{my.event}).

Variables

$eventEvent
(in listener only) The dispatched Event.

Modifiers

[string]
Optional tag to attach multiple listeners to the same event (e.g., @click[1], @click[2], etc.).
.prevent[boolean]
Call event.preventDefault() when triggered.
.stop[boolean]
Call event.stopPropagation() when triggered.
.once[boolean]
Register listener with { once: true }.
.passive[boolean]
Register listener with { passive: true }.
.capture[boolean]
Register listener with { capture: true }.
.self[boolean]
Trigger listener only if event.target is the element itself.
.attach["element" | "window" | "document"]
Attach listener to a different target (e.g., window or document).
.throttle[duration≈250ms]
Prevent listener from being called more than once during the specified time frame.
.debounce[duration≈250ms]
Delay listener execution until the specified time frame has passed without any activity.
.keys[string]
Specify which keys must be pressed for the listener to trigger on a KeyboardEvent.
  • The syntax for keys constraints is defined as follows:
    • Combine keys with a plus sign + (e.g., @keypress.keys[ctrl+space]).
    • Separate multiple combinations with a comma , (e.g., @keypress.keys[ctrl+space,shift+space]).
  • Supported keys and aliases:
    • alt for "Alt".
    • ctrl for "Control".
    • shift for "Shift".
    • meta for "Meta".
    • space for " ".
    • key for any key except "Alt", "Control", "Shift", and "Meta".
    • Any value returned by event.key.

Binding

:attribute="value"
Multiple Phase51 — ATTRIBUTE

Bind an element's attribute value.

<a :href="url">
  <!--...-->
</a>
1 Warning 3 Notes 1 TBD
Binding directives is not officially supported and is considered undefined behaviour.
:class and :style have specific handling described below.
Bind multiple attributes in a single directive using the shorthand :="object" (e.g. :="{ foo: 'bar', bar: true }").
Boolean attributes defined by the HTML spec are handled accordingly (removed when falsy).
Attributes with null or undefined values are removed.

:class="value"
Multiple Phase51 — ATTRIBUTE

Bind an element's class attribute.

<p :class="{ foo: true, bar: false }">
  <!--...-->
</p>
3 Notes
The expression can be:
  • A string of space-separated class names (e.g., "foo bar").
  • A Record<PropertyKey, boolean> mapping class names to their state (e.g., { foo: true, bar: false }).
  • An Array of the supported types (e.g., [ "foo", { bar: false }, [] ]).
The initial class attribute value is preserved.
Class names with at least one truthy value are treated as active.

:style="value"
Multiple Phase51 — ATTRIBUTE

Bind an element's style attribute.

<p :style="{ color: 'salmon' }">
  <!--...-->
</p>
3 Notes
The expression can be:
  • A string supported by HTMLElement.style.cssText (e.g., "color: blue;").
  • A Record<PropertyKey, unknown> mapping CSS properties to their values (e.g., { backgroundColor: "red", "border-color": "green", width: 1 }).
  • An Array of the supported types (e.g., [ "color: blue", { backgroundColor: "red" }, [] ]).
The initial style attribute value is preserved.
CSS properties are processed in the order they are defined, regardless of !important.

Modeling

::value="model"
Phase52 — ATTRIBUTE_MODEL_VALUE

Bind an <input>, <select> or <textarea> element's value attribute in a bi-directional manner.

<select ::value="foo">
  <!--<option>...</option>-->
</select>
2 Warnings 1 Restriction 1 Note .event .name .value .throttle .debounce .keys
<input type="checkbox"> and <select multiple> elements will bind to an array of values.
Using a modeled value within @input or @change expressions can cause precedence issues, as the model relies on these events to update. To avoid this, listen to the :: event, which is always triggered after the model has been updated.
Must be used on elements with a value property, such as <input>, <select>, or <textarea>. For other elements, use the :attribute directive.
You can use the shorthand syntax ::="model" instead of ::value="model".

Modifiers

.event[string="input"]
Change the Event that triggers the model update. Recommended events are "input" or "change".
.name[boolean]
Automatically set the input name attribute based on the attribute's value (e.g., <input ::.name="foo"> becomes <input name="foo">). The default is true for <input type="radio"> and <input type="checkbox">, and false for all other elements.
.value[boolean]
Initialize the model using the nullish coalescing operator and the input value attribute if present (e.g., <input ::.value="foo" value="bar"> assigns foo the value "bar" if it was nullish).
.throttle[duration≈250ms]
Prevent the listener from being called more than once during the specified time frame.
.debounce[duration≈250ms]
Delay listener execution until the specified time frame has passed without any activity.
.keys[string]
Specify which keys must be pressed for the listener to trigger on a KeyboardEvent. See @event.keys modifier for more information.

HTTP

%http="url"
Phase33 — HTTP_REQUEST

Perform a fetch() call that can be handled by %response directives.

<div %http="https://example.com">
  <!--...-->
</div>
3 Notes $event .follow .history .method .get .head .post .put .patch .delete
Without a %response directive, the request won't be performed automatically. Use %response.void if you want to trigger the request but ignore the response.
Valid URLs may be specified as is.
A new request is triggered for the same element if:
  • Its reference changes.
  • The evaluated URL changes.
Since predicting when a new request will be performed is challenging, use this directive only for read-only operations. For endpoints with side effects, consider the %@event directive.

Variables

$eventEvent | null
(in url expression only) The dispatched Event if triggered by a %@event directive, or null.

Modifiers

.follow[boolean=true]
Control whether fetch() should follow redirections.
.history[boolean]
Whether to update the browser history with history.pushState() for the target URL (must be the same origin).
.method[string]
Set the HTTP method (the value is uppercased). This modifier should not be used with its aliases.
.get[boolean]
Alias for .method[get].
.head[boolean]
Alias for .method[head]
.post[boolean]
Alias for .method[post]
.put[boolean]
Alias for .method[put]
.patch[boolean]
Alias for .method[patch]
.delete[boolean]
Alias for .method[delete]

%body="content"
Phase32 — HTTP_BODY

Set HTTP body for a %http directive.

<div %body.json="{foo:'bar'}">
  <!--...-->
</div>
$headers .type .header .text .form .json .xml

Variables

$headersHeaders
A Headers object containing all registered headers from %header directives attached to the element.

Modifiers

.type["text" | "form" | "json" | "xml"]
Format the body with the specified type: This modifier should not be used with one of its aliases.
.header[boolean=true]
Automatically set the Content-Type header when using a .type modifier:
  • text: set Content-Type: text/plain.
  • form: set Content-Type: application/json.
  • json: set Content-Type: application/x-www-form-urlencoded.
  • xml: set Content-Type: application/xml.
If the header was already set, it is overwritten.
.text[boolean]
Alias for .type[text].
.form[boolean]
Alias for .type[form].
.json[boolean]
Alias for .type[json].
.xml[boolean]
Alias for .type[xml].

%response="expression"
Defaultnull Multiple Phase34 — HTTP_CONTENT

Reacts to a %http directive's Response.

<div %http="'https://example.com'" %response.html>
  <!--...-->
</div>
$response $content [tag] .consume .void .text .html .json .xml .swap

Variables

$responseResponse
A Response object containing the fetched data.
$contentunknown
Contains the response.body (type depends on the modifier used).

Modifiers

[string]
Specify which HTTP status codes trigger this directive:
  • The syntax for status code constraints is defined as follows:
    • Define a range using a minus sign - between two numbers (e.g., %response[200-299]).
    • Specify multiple ranges and statuses by separating them with a comma , (e.g., %response[200,201-204]).
  • Supported aliases:
    • 2XX for 200-299.
    • 3XX for 300-399.
    • 4XX for 400-499.
    • 5XX for 500-599.
.consume["void" | "text" | "html" | "json" | "xml"]
Consume the response.body: This modifier should not be used with one of its aliases.
.void[boolean]
Alias for .consume[void].
.text[boolean]
Alias for .consume[text].
.html[boolean]
Alias for .consume[html].
.json[boolean]
Alias for .consume[json].
.xml[boolean]
Alias for .consume[xml].
.swap[boolean]

Consume body using response.text() and set target's outerHTML. This modifier takes precedence over the .consume modifier and makes it effectless, although if .consume[text] is set, swapped content will be escaped.

Any non-directive HTML attributes on the target will be applied to the swapped content elements.

%@event="listener"
Defaultnull Multiple Phase35 — HTTP_INTERACTIVITY

Listen for a dispatched Event and re-evaluates %http directive before reacting to its Response.

<button %http="https://example.com" %@click.html>
  <!--...-->
</button>
1 Restriction 2 Notes $event $response $content ...
Must be defined on an element that also possess a %http directive.
This is essentially a combination of %response and @event directives.
Target URL is still set by %http directive. As it is re-evaluated, you can however use the $event value to dynamically compute the target URL (e.g.%http="$event ? '/foo' : '/bar'"). All modifiers from %http directive are inherited, along with the RequestInit prepared by %header and %body directives.

Variables

$eventEvent
(in listener only) The dispatched Event.
$responseResponse
A Response object that contains the fetched data.
$contentunknown
A variable that contains the response.body (typing depends on which modifier is used).

Modifiers

...
Inherited from @event and %response directives. See their respective documentation for more information.

Processing

*once
Phase99 — POSTPROCESSING

Render an element once and skip subsequent updates.

<div *once>
  <!--...-->
</div>

*refresh="interval"
Phase99 — POSTPROCESSING

Reprocess an element at a specified interval (in seconds).

<div *refresh="1.5">
  <!--<time *text="new Date()"></time>-->
</div>
2 Warnings 4 Notes $refresh
Ensure proper context management to prevent unexpected errors.
Avoid using with iterative directives like *for as *refresh will be duplicated for each generated element.
The target element will be rendered regardless of detected changes. This is useful for updating content that cannot be directly observed, but use sparingly to avoid performance issues.
Set the interval to null to stop refreshing.
If the element is commented out by a directive, the refresh is automatically cleared.
Refresh operations are performed using setTimeout. New calls are scheduled when the directive is processed again, ensuring a consistent interval.

Variables

$refreshboolean
Indicates if the element is currently being refreshed.

*eval="expression"
Phase89 — CUSTOM_PROCESSING

Evaluate a JavaScript expression in the context of the element.

<div *eval="console.log('$data')">
  <!--...-->
</div>
1 Warning 1 Note
Use this directive sparingly, prefer alternative directives for better maintainability and security. This directive is intended for edge cases.
The expression runs after the element and all its children have been fully processed.

*skip
Phase2 — PREPROCESSING

Prevent an element from being processed.

<div *skip>
  <!--<p *text="foo"></p>-->
</div>

Testing

These directives are for development and testing purposes only. They help developers validate features and renderings before production.

Do not use in production environments.

~test="expression"
Multiple Phase10 — TESTING

Special directive for testing purposes.

<samp ~test[testing].text="'...'">
  <!--...-->
</samp>
2 Warnings [tag] .text .eval .comment .throw
For testing only. Use this directive to isolate and test custom directives without relying on others.
The modifiers may not be compatible with each other.

Modifiers

[string]
Specify any existing Phase name (e.g., ~test[testing], defaults to Phase.TESTING). The directive will execute during the specified phase before any other directive in that phase, allowing you to simulate specific scenarios.
.text[boolean]
Set the element's textContent with the expression result.
.eval[boolean]
Evaluate a JavaScript expression within the element's context.
.comment[boolean]
Convert the element to a Comment if the expression is truthy, and revert it otherwise.
.throw[boolean]
Throw an EvalError if the expression is truthy.

Unstable

New features that are still in the design phase are published in this package to gather community feedback. These features may undergo significant changes during development and might not be included in the stable release.

Use at your own risk.

*noop
Multiple Phase10 — TESTING

This directive does nothing.

<div *noop></div>

API

User API

This section is targeted at users who want to use mizu.js rendering and directives.

If you wish to develop custom directives, please refer to the Developer API section instead.

Client-side API

Full API documentation is available at jsr.io/@mizu/render/client.

Defaults directives

The following directives are enabled by default in the client-side API. You can customize the enabled directives by creating a custom instance rather than using the default one.

Client.context: Record<PropertyKey, any>

Rendering context.

All properties assigned to this object are available during rendering.

Changes to this object are reactive and will trigger a re-render of related elements. This is achieved using Context, which leverages Proxy handlers.

You cannot reassign this property directly to ensure reactivity is maintained. To achieve a similar effect, use Object.assign().

Server-side API

Full API documentation is available at jsr.io/@mizu/render/server.

Defaults directives

The following directives are enabled by default in the server-side API. You can customize the enabled directives by creating a custom instance rather than using the default one.

Server.generate(sources: Array<StringSource | GlobSource | CallbackSource | URLSource>, options?: ServerGenerateOptions) => Promise<void>

Generate static files from various sources.

Options:

  • output: Specify the path to the output directory.
  • clean: Empty the output directory before generating files.

Supported sources:

  • StringSource: Generate content from raw strings.
  • GlobSource: Generate content from local files matching the provided glob patterns.
  • CallbackSource: Generate content from callback returns.
  • URLSource: Generate content from fetched URLs.

Each source can be templated using mizu rendering by passing a render option.

const mizu = new Server({ directives: ["@mizu/test"], generate: { output: "/fake/output" } })
await mizu.generate(
  [
    // Copy content from strings
    [ "<p>foo</p>", "string.html" ],
    [ "<p ~test.text='foo'></p>", "string_render.html", { render: { context: { foo: "bar" } } } ],
    // Copy content from local files
    [ "**\/*", "public", { directory: "/fake/static" } ],
    [ "*.html", "public", { directory: "/fake/partials", render: { context: { foo: "bar "} } } ],
    // Copy content from callback return
    [ () => JSON.stringify({ foo: "bar" }), "callback.json" ],
    [ () => `<p ~test.text="'foo'"></p>`, "callback.html", { render: { context: { foo: "bar" } } } ],
    // Copy content from URL
    [ new URL(`data:text/html,<p>foobar</p>`), "url.html" ],
    [ new URL(`data:text/html,<p ~test.text="foo"></p>`), "url_render.html", { render: { context: { foo: "bar" } } } ],
  ],
  // No-op: do not actually write files and directories
  { fs: { readdir: () => Promise.resolve([] as string[]), mkdir: () => null as any, write: () => null as any } },
)

Developer API

This section is targeted at developers who want to create custom mizu.js rendering and directives.

If you wish to render templates, please refer to the User API section instead.

Directive API

Full API documentation is available at jsr.io/@mizu/internal/engine.

This section covers the Directive interface, essential for creating custom directives. Each built-in directive is also an instance of this interface. It follows the steps outlined in the Rendering concept.

When manipulating the DOM, use the Renderer methods to ensure compatibility between virtual and real DOMs. These methods also provide helpful tools to simplify the creation of custom directives.

Directive.name: string | RegExp
Readonly

Directive name.

The Renderer.render() method uses this value to determine whether Directive.execute() should be called for the processed node.

The name should be prefixed to avoid conflicts with regular attribute names and must be unique among other Renderer.directives. Renderer.load() will use this value to check whether the directive is already loaded.

If the directive name is dynamic, a RegExp may be used instead of a string. In this case, Directive.prefix should be specified.

const foo = {
  name: "*foo",
  phase: Phase.UNKNOWN,
} as Directive & { name: string }

Directive?.multiple: boolean
Readonly

Indicates whether the directive can be specified multiple times on the same node.

If set to false, a warning will be issued to users attempting to apply it more than once.

const foo = {
  name: /^\/(?<value>)/,
  prefix: "/",
  phase: Phase.UNKNOWN,
  multiple: true
} as Directive & { name: RegExp; prefix: string }

Directive?.typings: Typings
Readonly

Typings for directive parsing.

For more information, see Renderer.parseAttribute().

const typings = {
  type: Boolean,
  modifiers: {
    foo: { type: Boolean, default: false },
  }
}

const foo = {
  name: "*foo",
  phase: Phase.UNKNOWN,
  typings,
  async execute(renderer, element, { attributes: [ attribute ], ...options }) {
    console.log(renderer.parseAttribute(attribute, this.typings, { modifiers: true }))
  }
} as Directive<null, typeof typings> & { name: string }

Directive?.default: string
Readonly

Default value.

This value should be used by directive callbacks when the Attr.value is empty.

const foo = {
  name: "*foo",
  phase: Phase.UNKNOWN,
  default: "bar",
  async execute(renderer, element, { attributes: [ attribute ], ...options }) {
    console.log(attribute.value || this.default)
  }
} as Directive & { name: string; default: string }

Directive?.init: Function
Readonly

Directive initialization callback.

This callback is executed once during when Renderer.load() loads the directive. It should be used to set up dependencies, instantiate directive-specific caches (via Renderer.cache()), and perform other initialization tasks.

If a cache is instantiated, it is recommended to use the Directive<Cache> generic type to ensure type safety when accessing it in Directive.setup(), Directive.execute(), and Directive.cleanup().

const foo = {
  name: "*foo",
  phase: Phase.UNKNOWN,
  async init(renderer) {
    renderer.cache(this.name, new WeakSet())
  },
} as Directive<WeakSet<HTMLElement | Comment>> & { name: string }

Directive?.setup: Function
Readonly

Directive setup callback.

This callback is executed during Renderer.render() before any Directive.execute() calls.

A partial object can be returned to update the rendering State, and the eligibility.

If false is returned, the entire rendering process for this node is halted.

This method is executed regardless of the directive's presence on the node.

const foo = {
  name: "*foo",
  phase: Phase.UNKNOWN,
  async setup(renderer, element, { cache, context, state }) {
    if ((!renderer.isHtmlElement(element)) || (element.hasAttribute("no-render"))) {
      return false
    }
  },
} as Directive & { name: string }

Directive?.execute: Function
Readonly

Directive execution callback.

This callback is executed during Renderer.render() if the rendered node has been marked as eligible.

A node is considered eligible if at least one of the following conditions is met:

A partial object can be returned to update the rendering Context, State, and the rendered HTMLElement (or Comment).

If final: true is returned, the rendering process for this node is stopped (all Directive.cleanup() will still be called).

const foo = {
  name: "*foo",
  phase: Phase.UNKNOWN,
  async execute(renderer, element, { attributes: [ attribute ], ...options }) {
    console.log(`${await renderer.evaluate(element, attribute.value || "''", options)}`)
    return { state: { $foo: true } }
  },
} as Directive & { name: string }

Directive?.cleanup: Function
Readonly

Directive cleanup callback.

This callback is executed during Renderer.render() after all Directive.execute() have been applied and all Element.childNodes have been processed.

This method is executed regardless of the directive's presence on the node, and regardless of whether a Directive.execute() returned with final: true.

const foo = {
  name: "*foo",
  phase: Phase.UNKNOWN,
  async cleanup(renderer, element, { cache, context, state }) {
    console.log("Cleaning up")
  }
} as Directive & { name: string }

Internal API

Full API documentation is available at jsr.io/@mizu/internal.

This package includes several APIs used by mizu.js, such as the Renderer, virtual DOM implementations, the testing framework, and more. These symbols are documented in the source code and are intended for development environments.

To Be Defined

mizu.js currently follows ZeroVer versioning. This means breaking changes may occur in minor versions, even for previously defined features.

It will eventually stabilize and follow SemVer versioning after more feedback and testing.

Some aspects of mizu.js are not fully defined yet. While these are mentioned in the documentation, they are gathered here for easy reference.

Relying on undefined behavior in your applications is strongly discouraged, as these behaviors may change in future versions without being considered breaking changes. Additionally, any unusual behavior encountered is not considered a bug until properly specified.

If you wish to participate in the design and definition of these features, check out the issues tagged with spec.

Global
Repeating a modifier on a single directive is considered undefined behavior.
Global
Although @libs/reactive can track function calls, mizu.js currently does not support reprocessing elements that depend on them. This is because expressions are re-evaluated with each re-render, which would re-trigger the function call. Future versions of mizu.js may introduce a mechanism to prevent this behavior.
Global
Dynamically adding and removing mizu.js directives is not officially supported. While it may work currently due to the rendering engine's lack of optimization, future versions may cache directives to enhance performance. This change would prevent dynamically added directives from being processed.
Global
Using conflicting directives on the same element results is considered undefined behavior.
There is currently no special handling for <template> elements, but future versions may introduce specific behavior for these elements.
There is currently no special handling for <template> elements, but future versions may introduce specific behavior for these elements.
There is currently no distinction between let, const and var declarations inside for loops, but future versions may introduce specific behavior for these.
There is currently no special handling for <template> elements, but future versions may introduce specific behavior for these elements.
There is currently no special handling for <template> elements, but future versions may introduce specific behavior for these elements.
There is currently no distinction between double mustaches ({{ and }}) and triple mustaches ({{{ and }}}), but future versions may introduce specific behavior for these.
Binding directives is not officially supported and is considered undefined behaviour.