Introduction: Basics

Your First Luna Component

Let's build a simple counter to understand Luna's core concepts.

Quick Start

The fastest way to get started is using the CLI:

npx @luna_ui/luna new myapp --mbt
cd myapp
moon update
npm install
npm run dev

This creates a new MoonBit project with Vite and vite-plugin-moonbit configured.

A Counter in MoonBit

using @server_dom { div, p, button, text }
using @luna { signal }

fn counter() -> @luna.Node {
  let count = signal(0)

  div([
    p([text("Count: " + count.get().to_string())]),
    button([text("Increment")]),
  ])
}

Breaking It Down

1. Imports

using @server_dom { div, p, button, text }
using @luna { signal }
  • @server_dom provides HTML element factories for server-side rendering

  • @luna provides reactive primitives like signal

2. Create Reactive State

let count = signal(0)

signal(0) creates a reactive value that:

  • Starts at 0

  • Can be read with .get()

  • Can be updated with .set() or .update()

3. Build the DOM

div([
  p([text("Count: " + count.get().to_string())]),
  button([text("Increment")]),
])

Elements are created with factory functions:

  • div([...]) creates a <div> with children

  • text("...") creates a text node

Rendering to HTML

To render the component:

fn main {
  let node = counter()
  let html = @renderer.render_to_string(node)
  println(html)
}

Output:

<div><p>Count: 0</p><button>Increment</button></div>

Server-Side vs Client-Side

In Luna's Islands Architecture:

WhereWhat
Server (MoonBit)Renders initial HTML
Client (TypeScript)Adds interactivity via hydration

The MoonBit code above renders the initial HTML. For the button to actually increment the counter, you need a client-side TypeScript component that hydrates the island.

Complete Example with Island

using @server_dom { island, div, p, button, text }
using @luna { signal, Load }

fn counter_island(initial : Int) -> @luna.Node {
  island(
    id="counter",
    url="/static/counter.js",
    state=initial.to_string(),
    trigger=Load,
    children=[
      div([
        p([text("Count: " + initial.to_string())]),
        button([text("Increment")]),
      ])
    ],
  )
}

This renders HTML with hydration attributes:

<div luna:id="counter" luna:url="/static/counter.js" luna:state="0" luna:client-trigger="load">
  <div><p>Count: 0</p><button>Increment</button></div>
</div>

The client-side TypeScript code then hydrates this element. See Islands Basics for details.

Try It

  1. Create a component that shows "Hello, Luna!"

  2. Add a signal for a name and display "Hello, {name}!"

Next

Learn about Signals โ†’