HTML syntax

Nue extends standard HTML with expressions, control flow, and components. Everything works on both server and client unless marked client-only.

Standard HTML

Every HTML document is a valid Nue document:

<!doctype html>

<html>
  <head>...</head>

  <body>
    <article>
      <button onclick="history.go(-1)">Back</button>
      <button popovertarget="confirm-delete">Delete</button>
    </article>

    <dialog id="confirm-delete">
      <h2>Delete user?</h2>
    </dialog>
  </body>
</html>

Expressions

Insert dynamic values with curly brackets:

<!-- text content -->
<span>{ username }</span>

<!-- JavaScript expressions -->
<p>{ username.toUpperCase() }</p>

<!-- unescaped HTML -->
<div>{{ markdown(description) }}</div>
<div>{{ renderContent(article) }}</div>

<!-- triple brackets also supported -->
<div>{{{ userSubmittedContent }}}</div>

Attributes

Dynamic attributes use the same expression syntax:

<!-- attribute values -->
<time datetime="{ date.toISOString() }">

<!-- boolean attributes (falsy values remove the attribute) -->
<button disabled="{ is_disabled }">

<!-- class name interpolation -->
<div class="gallery { type }">

<!-- conditional classes -->
<div class="[ is-active: isActive, has-error: hasError ]">

<!-- combine static and dynamic -->
<div class="gallery { type } [ is-active: isActive ]">

Loops

Render lists with :each:

<!-- basic loop -->
<li :each="item in items">{ item.name }</li>

<!-- with index -->
<li :each="item, i in items">
  { i }: { item.name }
</li>

<!-- destructuring -->
<li :each="{ name, price } in products">
  { name } costs { price }
</li>

<!-- loop objects -->
<li :each="[key, val] in Object.entries(data)">
  { key } = { val }
</li>

<!-- template loops (no wrapper element) -->
<dl>
  <template :each="term in glossary">
    <dt>{ term.word }</dt>
    <dd>{ term.definition }</dd>
  </template>
</dl>

Conditionals

Control rendering with :if:

<p :if="count > 100">Too many!</p>
<p :else-if="count > 10">Getting there</p>
<p :else>{ count } items</p>

<!-- combine with loops (condition evaluated first) -->
<ul :if="items.length">
  <li :each="item in items">{ item }</li>
</ul>
<p :else>No items</p>

Components

Reusable pieces of UI:

<!-- define a component -->
<product-card>
  <h3>{ name }</h3>
  <p>{ price }</p>

  <script>
    // default values
    this.name = 'Untitled'
    this.price = 0
  </script>
</product-card>

<!-- use the component -->
<product-card/>

<!-- pass properties -->
<product-card :name="Coffee" :price="12"/>

<!-- pass data variables -->
<product-card :name="productName" :price="productPrice"/>

<!-- shorthand (passes the name and price variables) -->
<product-card :name :price/>

<!-- regular attributes (no colon prefix) are rendered -->
<product-card id="featured" class="highlight"/>

<!-- loop components -->
<product-card :each="item in products" :bind="item"/>

Component root element

Components default to <div> wrapper. Change with :is:

<!-- this component renders as <figure> -->
<image-card :is="figure">
  <img src="{ url }">
  <figcaption>{ caption }</figcaption>
</image-card>

Event handlers

Client-only - Handle user interactions:

<counter>
  <button :onclick="count++">{ count }</button>

  <script>
    this.count = 0
  </script>
</counter>

<!-- method handlers -->
<counter>
  <button :onclick="increment">+</button>
  <button :onclick="decrement">-</button>
  <p>Count: { count }</p>
  <p>Double: { double }</p>

  <script>
    this.count = 0

    increment() {
      this.count++
    }

    decrement() {
      if (this.count > 0) this.count--
    }

    // getter methods are supported
    get double() {
      return this.count * 2
    }
  </script>
</counter>

<!-- event object -->
<form :onsubmit="handleSubmit">
  <script>
    handleSubmit(e) {
      // forms automatically call e.preventDefault()
      console.log('Submitted:', e.target)
    }
  </script>
</form>

Lifecycle methods

Client-only - Run code at specific moments:

<user-profile>
  <h2>{ user.name }</h2>

  <script>
    // before mounting to DOM
    onmount() {
      console.log('About to mount')
    }

    // after mounting to DOM
    mounted() {
      console.log('Mounted!')
    }

    // before updating
    onupdate() {
      console.log('About to update')
    }

    // after updating
    updated() {
      console.log('Updated!')
    }
  </script>
</user-profile>

Manual updates

Client-only - Trigger component re-render with new data:

this.update(data)

Event handlers update automatically, but async operations or external events (like web socket messages) need manual updates:

<script>
  async mounted() {
    const data = await fetch('/api/user')
    const user = await data.json()

    // Manual update required after async operations
    this.update({ user })
  }
</script>

Dynamic mounting

Client-only - Mount components programmatically inside a single-page app:

this.mount(name, target, data)

name - Component name

target - DOM element or CSS selector

data - Optional data to pass to component

<my-app>

  <article/>

  <script>
    state.on('id', ({ id }) => {
      this.mount(id ? 'user-details' : user-list', 'article')
    })
  </script>

</my-app>

See single-page apps for routing patterns.

Shared scripts

Define functions and data for multiple components:

<!-- top-level script -->
<script>
  // available to all components
  const TAX_RATE = 0.08

  function formatPrice(num) {
    return '$' + num.toFixed(2)
  }
</script>

<!-- use in components -->
<product-card>
  <p>{ formatPrice(price) }</p>
  <p>Tax: { formatPrice(price * TAX_RATE) }</p>

  <script>
    this.price = 10
  </script>
</product-card>

<!-- another component definition -->
<shopping-cart>
  <p>{ formatPrice(price) }</p>

  <script>
    // ...
  </script<
</shopping-cart>

JavaScript imports

Client-only - Import external modules:

<script>
  import { formatDistance } from './utils.js'
  import { store } from './store.js'
</script>

<!-- imported functions available in templates -->
<article>
  <time>{ formatDistance(date) }</time>
  <p>Cart items: { store.cart.length }</p>
</article>

Passthrough scripts

Server-only - Scripts with type or src pass through unchanged:

<!-- these render as-is to the client -->
<script src="/analytics.js"></script>

<script type="module">
  console.log('This runs on the client')
</script>

Slots

Component composition pattern:

<!-- component with slot -->
<card>
  <div class="card">
    <slot/>
  </div>
</card>

<!-- using the slot -->
<card>
  <h2>This goes inside the card</h2>
  <p>So does this</p>
</card>

<!-- multiple instances -->
<card :each="post in posts">
  <h2>{ post.title }</h2>
  <p>{ post.excerpt }</p>
</card>

CSS variables

Pass design tokens without inline styles:

<!-- renders as style="--spacing: 2rem" -->
<section --spacing="2rem">
  <style>
    section {
      padding: var(--spacing);
    }
  </style>
</section>

<!-- dynamic values -->
<div --columns="{ columnCount }">

No inline styles or class overloading. Your design system stays the single source of truth.