Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/nuejs/nue/llms.txt

Use this file to discover all available pages before exploring further.

Nuedom is the component system at the heart of Nue. Rather than expressing UI as JavaScript functions that return virtual nodes, Nuedom treats components as extended HTML — familiar markup augmented with just enough syntax to handle dynamic data, user events, and conditional rendering. The result is components that look like HTML, because they are HTML.

How Nue components differ from React

The architectural difference is fundamental. React components are JavaScript functions. The JSX they return is transpiled to React.createElement() calls, which build a virtual DOM tree that gets reconciled on each render. A simple form becomes a file full of hooks, refs, and effect handlers. Nue components are HTML documents. The parser reads standard HTML extended with : prefixed attributes for event binding and { } expressions for data interpolation. There is no virtual DOM — Nue’s AST maps directly to DOM operations.
import { useState, useRef } from 'react'

export function ContactForm() {
  const [status, setStatus] = useState('idle')
  const formRef = useRef(null)

  async function handleSubmit(e) {
    e.preventDefault()
    await fetch('/api/contact', {
      body: new FormData(formRef.current),
      method: 'POST'
    })
    setStatus('sent')
  }

  return (
    <form ref={formRef} onSubmit={handleSubmit}>
      <input type="email" name="email" required />
      <textarea name="message" minLength={10} required />
      <button>Send</button>
      {status === 'sent' && <p>Message sent!</p>}
    </form>
  )
}
<!-- form with native validation -->
<form :onsubmit="submit">
  <input type="email" name="email" required>
  <textarea name="message" minlength="10" required></textarea>
  <button>Send</button>

  <script>
    async submit(e) {
      await fetch('/api/contact', {
        body: new FormData(e.target),
        method: 'POST'
      })
      success.showPopover()
    }
  </script>
</form>

<!-- native dialog for success -->
<dialog id="success" popover>
  <h2>Message sent!</h2>
  <button popovertarget="success">Close</button>
</dialog>
The Nue version uses a native <dialog> with the Popover API for the success state, native form validation for the required fields, and a single method defined in an inline <script>. No hooks, no refs, no state variables.

Dynamic expressions

Data bindings use { } curly-brace syntax inside text content and attribute values. Expressions are compiled to arrow functions that receive the component’s data object as _:
<article>
  <h1>{ title }</h1>
  <p class="byline">By { author } — { date }</p>
  <img src="{ hero_image }" alt="{ title }">
</article>
The compiler (compileFn in compiler.js) transforms _.foo + 1 into _=>(_.foo + 1), making simple property references as concise as possible while supporting full JavaScript expressions:
<!-- Simple property -->
<span>{ count }</span>

<!-- Expression -->
<span>{ count > 0 ? count + ' items' : 'Empty' }</span>

<!-- Method call -->
<time>{ formatDate(date) }</time>

Event listeners

Event binding uses :on prefixed attributes. The event name follows the colon — :onclick, :oninput, :onsubmit, :onchange. The value is a method name or inline expression:
<counter>
  <p>Count: { count }</p>
  <button :onclick="increment">+</button>
  <button :onclick="decrement">-</button>

  <script>
    count = 0

    increment() { this.count++ }
    decrement() { this.count-- }
  </script>
</counter>
The compiler produces event handler functions with signature (_,$e) => where _ is the component instance and $e is the DOM event. A method name like submit becomes (_,$e) => submit($e). An inline expression with semicolons becomes a block: (_,$e) => { expr1; expr2 }. For input elements, you can bind directly to state without a method:
<search-box>
  <input :oninput="query = $event.target.value" placeholder="Search...">
  <p>{ results.length } results for "{ query }"</p>
</search-box>

Conditional rendering

Use the :if, :else-if, and :else attributes to conditionally render elements:
<user-status>
  <p :if="user.isLoggedIn">Welcome, { user.name }!</p>
  <p :else-if="user.isGuest">Browsing as guest.</p>
  <p :else>Please sign in.</p>
</user-status>

Loops

The :for attribute iterates over arrays and objects. The value uses JavaScript destructuring syntax:
<product-list>
  <ul>
    <li :for="item in items">
      <a href="{ item.url }">{ item.name }</a>
      <span class="price">{ item.price }</span>
    </li>
  </ul>
</product-list>
Combine with conditionals for filtered rendering:
<nav>
  <a :for="link in links" :if="link.visible" href="{ link.href }">
    { link.label }
  </a>
</nav>

Composing components

Nue components reference each other by tag name. If a component file defines a <user-avatar> component and it is in scope (either in the same file or imported via a library file), you use it like any HTML element:
<user-card>
  <user-avatar :src="avatar" :size="48" />
  <div class="info">
    <h3>{ name }</h3>
    <p>{ role }</p>
  </div>
</user-card>

<user-avatar>
  <img :src="src" :width="size" :height="size" alt="{ name }">
</user-avatar>
Props flow down as attributes using the : binding syntax on the parent, and appear as plain data properties on the child.

Library files

A file is treated as a component library when the is_lib flag is set. This happens when the HTML file uses the <!dhtml lib> or html lib doctype declaration. Library components are loaded and shared across all pages that depend on them. In asset.js, the components() method collects all library dependencies:
// From asset.js — collecting library components
for (const asset of (await assets()).filter(el => el.is_html)) {
  const ast = await asset.parse()
  if (ast.is_lib) {
    const same_type = is_dhtml == ast.is_dhtml
    const isomorphic = doctype.startsWith('html+dhtml')
    if (isomorphic || same_type || forced) ret.push(...ast.lib)
  }
}
A library file in your project might look like:
<!dhtml lib>

<user-avatar>
  <img :src="src" :width="size" :height="size" alt="">
</user-avatar>

<user-badge>
  <span class="badge badge-{ type }">{ label }</span>
</user-badge>
Place shared library files in @shared/ so they are available to all applications in your project.

Server-side vs client-side rendering

Nue uses the doctype declaration to determine how a component file is rendered:
1

Static HTML (server-side only)

A file with no special doctype or <!html> is rendered entirely on the server. No JavaScript is sent to the browser. Use this for content components like headers, footers, and article layouts.
<!html>

<site-header>
  <nav>
    <a :for="link in links" href="{ link.href }">{ link.label }</a>
  </nav>
</site-header>
2

Dynamic HTML (client-side)

A file with <!dhtml> compiles to JavaScript and runs in the browser. The server renders a stub element; the client hydrates it with full interactivity.
<!dhtml>

<shopping-cart>
  <p>{ items.length } items — total: { total }</p>
  <button :onclick="checkout">Checkout</button>

  <script>
    get total() {
      return this.items.reduce((s, i) => s + i.price, 0)
    }

    async checkout() {
      await fetch('/api/checkout', { method: 'POST' })
    }
  </script>
</shopping-cart>
3

Isomorphic (both)

A html+dhtml lib doctype runs the same component both server-side (for fast initial paint) and client-side (for interactivity). Useful for components like search that benefit from both.
The renderHTML function in page.js checks the is_dhtml flag from the parsed AST and routes to either renderNue (server rendering) or renderDHTML (client compilation with a server-rendered stub).
Library files (is_lib: true) are skipped during direct rendering — they are only included as dependencies of page files that reference their components.

Build docs developers (and LLMs) love