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.

Nue’s model for single-page apps starts from the same premise as its content sites: HTML is the structure of the web, and it should read like HTML. Rather than writing JavaScript functions that return objects approximating a DOM tree, you write actual HTML with declarative data bindings. Business logic lives in separate pure JavaScript modules. The result is a clean split between structure, behavior, and presentation that makes apps easier to read, test, and maintain.

The SPA model

When Nue encounters an index.html file in a project directory, it treats that directory as a single-page app. All assets in that directory — HTML component files, JS modules, CSS files — are bundled together and scoped to the app. Assets in @shared/ remain globally available.
dashboard/
  index.html SPA entry point
  main.js app bootstrap / business logic
  api.js data fetching
  state.js application state
  ui/
    sidebar.html layout component
    chart.html chart component
    table.html data table component
The entry index.html initializes the app and acts as the root component. Other .html files in the app directory are UI components picked up automatically by Nue’s dependency resolver.

Semantic HTML with data bindings

Nue’s template language is HTML extended with a small set of attributes for dynamic behavior. There is no JSX, no template literals, and no JavaScript expressions in markup beyond simple bindings.
<!-- ui/user-card.html -->
<article class="user-card">
  <img :src="user.avatar" :alt="user.name">
  <div class="info">
    <h2>{ user.name }</h2>
    <p class="role">{ user.role }</p>
    <span :class="{ active: user.online }">
      { user.online ? 'Online' : 'Offline' }
    </span>
  </div>
  <button :onclick="remove" :disabled="user.isOwner">Remove</button>

  <script>
    remove() {
      this.$emit('remove', this.user.id)
    }
  </script>
</article>
Key syntax elements:
SyntaxPurpose
{ expr }Text interpolation
:attr="expr"Dynamic attribute binding
:class="{ name: cond }"Conditional class binding
:for="item in list"List rendering
:if="cond"Conditional rendering
:onclick="handler"Event listener
<slot />Content projection

How nuedom compiles templates

Nuedom parses .html component files and produces an AST that maps directly to DOM operations. When Nue’s build detects that a parsed HTML file has is_dhtml: true — meaning it contains dynamic expressions — the asset is compiled to a .html.js module rather than static HTML.
// asset.js — extension selection
async function toExt() {
  if (file.is_html) {
    const { is_dhtml } = await parse()
    if (is_dhtml) return '.html.js'   // dynamic component → JS module
  }
  return file.is_md ? '.html' : file.is_ts ? '.js' : file.ext
}
The compiled module contains the minimal JavaScript needed to create DOM nodes, attach event listeners, and perform targeted updates when data changes. There is no virtual DOM and no diffing algorithm — when a reactive property changes, only the specific DOM nodes bound to that property are updated.
A .html file with no dynamic expressions compiles to static HTML, not JavaScript. Nue only produces a JS module when the template actually requires dynamic behavior.

Business logic in pure JS modules

Application logic in Nue apps lives in separate .js or .ts files, completely independent from the UI components. These modules export plain functions and objects that components import and use.
// api.js — pure data fetching, no UI concerns
export async function fetchUsers(filter = {}) {
  const params = new URLSearchParams(filter)
  const res = await fetch(`/api/users?${params}`)
  if (!res.ok) throw new Error(`Failed to fetch users: ${res.status}`)
  return res.json()
}

export async function removeUser(id) {
  const res = await fetch(`/api/users/${id}`, { method: 'DELETE' })
  if (!res.ok) throw new Error(`Failed to remove user: ${res.status}`)
}
// state.js — application state, no UI concerns
export const appState = {
  users: [],
  loading: false,
  error: null,

  async load(filter) {
    this.loading = true
    try {
      this.users = await fetchUsers(filter)
    } catch (e) {
      this.error = e.message
    } finally {
      this.loading = false
    }
  }
}
Components import from these modules directly:
<!-- ui/user-list.html -->
<section>
  <p :if="state.loading">Loading...</p>
  <p :if="state.error" class="error">{ state.error }</p>

  <ul :if="!state.loading">
    <li :for="user in state.users">
      <user-card :user="user" @remove="state.removeUser" />
    </li>
  </ul>

  <script>
    import { appState } from '../state.js'

    constructor() {
      this.state = appState
      appState.load()
    }
  </script>
</section>

No virtual DOM

Nue does not use a virtual DOM. React’s reconciliation approach requires maintaining an in-memory copy of the entire component tree to diff against on every render. Nue’s AST maps directly to DOM operations, so updates are surgical: when user.online changes, exactly the class binding and text node tied to that property are updated. No subtree comparison, no unnecessary re-renders. This has practical consequences:
  • Smaller runtime — no reconciler means the Nue runtime is a fraction of the size of React or Vue
  • Predictable updates — you can reason about what changes without understanding a diffing algorithm
  • No key prop management — list rendering updates the affected items without requiring unique key attributes to help a differ

Contrast with React/JSX

// UserCard.jsx
import { useState } from 'react'

export function UserCard({ user, onRemove }) {
  return (
    <article className="user-card">
      <img src={user.avatar} alt={user.name} />
      <div className="info">
        <h2>{user.name}</h2>
        <p className="role">{user.role}</p>
        <span className={user.online ? 'active' : ''}>
          {user.online ? 'Online' : 'Offline'}
        </span>
      </div>
      <button
        onClick={() => onRemove(user.id)}
        disabled={user.isOwner}
      >
        Remove
      </button>
    </article>
  )
}
The structural difference is that Nue’s template is valid HTML a browser can parse directly. The JSX equivalent is a JavaScript function that returns a description of what HTML might look like. A designer working in Nue’s template can apply CSS, rearrange elements, and use standard HTML attributes without reading any JavaScript. The same task in JSX requires understanding the component’s function signature and React’s attribute naming conventions (className, onClick, etc.).

Using native browser features

Because Nue components compile to real DOM elements, all standard browser capabilities work without any wrapping or polyfilling:
<!-- Native dialog with popover API -->
<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'
      })
      this.$refs.success.showPopover()
    }
  </script>
</form>

<dialog id="success" popover>
  <h2>Message sent!</h2>
  <button popovertarget="success">Close</button>
</dialog>
Native form validation, <dialog>, popover, scroll-snap, and container queries all work as documented by browser vendors — no adapter layer required.
When you reach for a UI pattern, check if a native HTML element already provides it before writing component logic. <details>/<summary> for accordions, <dialog> for modals, <input type="range"> for sliders — the platform covers more than most developers expect.

Build docs developers (and LLMs) love