Getting Started with Nectar
This guide walks you through installing Nectar, writing your first program, and building a complete application.
Prerequisites
Nectar’s compiler is written in Rust. You need:
- Rust toolchain (1.70 or later) – install from rustup.rs
- A modern web browser – for viewing compiled output (Chrome, Firefox, Safari, or Edge)
Verify your Rust installation:
rustc --version
cargo --version
Installation
Build from Source
Clone the Nectar repository and build the compiler:
git clone https://github.com/HibiscusConsulting/nectar-lang.git
cd nectar-lang
cargo build --release
The compiled binary is at target/release/nectar. Add it
to your PATH:
export PATH="$PWD/target/release:$PATH"
Verify the installation:
nectar --version
Your First Nectar Program
Create a file called hello.nectar:
component Hello(name: String) {
render {
<div>
<h1>"Hello from Nectar!"</h1>
<p>{name}</p>
</div>
}
}
This defines a Hello component that takes a
name prop and renders it inside a <p>
tag.
Compiling
Compile to WebAssembly Text Format (WAT):
nectar build hello.nectar
This produces hello.wat. To compile directly to binary
WebAssembly:
nectar build hello.nectar --emit-wasm
This produces hello.wasm.
Running in the Browser
You need three files in the same directory to run your component:
hello.wasm— the compiled WebAssembly modulecore.js— the Nectar runtime (copied from the repo)index.html— the entry point that loads and runs your component
Step 1: Copy the runtime
The Nectar runtime lives in the repository. Copy it to your working directory:
cp nectar-lang/runtime/modules/core.js .
(If you installed Nectar via package manager, the path will vary; check your installation directory.)
Step 2: Create index.html
<!DOCTYPE html>
<html>
<head><title>My Nectar App</title></head>
<body>
<div id="app"></div>
<script type="module">
import { instantiate } from './core.js';
instantiate('hello.wasm').then(inst => inst.exports.main());
</script>
</body>
</html>
Step 3: Use the built-in dev server
nectar dev --src . --port 3000
Open http://localhost:3000 to see your component.
The dev server will find your index.html, which imports
core.js, which loads hello.wasm. All three
files must be in the directory you run nectar dev from.
Alternative: Manual static server
If you prefer a different static server (Python, Node, etc.), make
sure all three files (hello.wasm, core.js,
index.html) are in the served directory.
Adding State and Reactivity
Let’s build a counter. Create counter.nectar:
component Counter(initial: i32) {
let mut count: i32 = initial;
fn increment(&mut self) {
self.count = self.count + 1;
}
fn decrement(&mut self) {
self.count = self.count - 1;
}
render {
<div>
<h2>"Counter"</h2>
<span>{self.count}</span>
<button on:click={self.increment}>"+1"</button>
<button on:click={self.decrement}>"-1"</button>
</div>
}
}
Key concepts:
let mut countdeclares mutable state.fn increment(&mut self)is a method that mutates the component state. The&mut selfparameter means it borrows the component mutably.on:click={self.increment}binds the click event to the method.{self.count}in the template reactively displays the current count. Whencountchanges, only this DOM node updates – no virtual DOM diffing needed.
Compile and run:
nectar build counter.nectar --emit-wasm
Handling Events
Nectar uses the on:event syntax for DOM events. The
handler is any expression, typically a method reference:
component Form() {
let mut value: String = "";
fn handle_submit(&mut self) {
// Process the form
println(f"Submitted: {self.value}");
self.value = "";
}
render {
<div>
<input
value={self.value}
placeholder="Enter text..."
/>
<button on:click={self.handle_submit}>"Submit"</button>
</div>
}
}
Common events: click, submit,
input, change, mouseover,
mouseout, keydown, keyup.
Fetching Data from an API
Nectar has first-class support for HTTP communication via the
fetch keyword:
struct Post {
id: u32,
title: String,
body: String,
}
store PostService {
signal posts: [Post] = [];
signal loading: bool = false;
async action fetch_posts(&mut self) {
self.loading = true;
let response = await fetch("https://jsonplaceholder.typicode.com/posts");
if response.status == 200 {
self.posts = response.json();
}
self.loading = false;
}
async action create_post(&mut self, title: String, body: String) {
let response = await fetch("https://jsonplaceholder.typicode.com/posts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: f"{{\"title\": \"{title}\", \"body\": \"{body}\"}}",
});
if response.status == 201 {
let new_post: Post = response.json();
self.posts.push(new_post);
}
}
}
component PostList() {
render {
<div>
<h1>"Posts"</h1>
{if PostService::get_loading() {
<div>"Loading..."</div>
}}
<ul>
{for post in PostService::get_posts() {
<li>
<h3>{post.title}</h3>
<p>{post.body}</p>
</li>
}}
</ul>
</div>
}
}
Key concepts:
storedefines global reactive state accessible from any component.async actiondeclares an asynchronous action that can useawait.fetch(url, options)makes HTTP requests with method, headers, and body.- Components read store state with
StoreName::get_field()and dispatch actions withStoreName::action_name().
Building a Complete Todo App
Let’s walk through todo.nectar, a complete todo
application demonstrating structs, enums, ownership, and components.
Step 1: Define Data Types
struct Todo {
id: u32,
text: String,
done: bool,
}
enum Filter {
All,
Active,
Completed,
}
Todo is a simple struct holding each item’s data.
Filter is an enum for the three filter states.
Step 2: Build the Component
component TodoApp() {
let mut todos: [Todo] = [];
let mut next_id: u32 = 0;
let mut filter: Filter = Filter::All;
The component maintains a list of todos, a counter for generating unique IDs, and the current filter.
Step 3: Add Business Logic
fn add_todo(&mut self, text: String) {
let todo = Todo {
id: self.next_id,
text: text,
done: false,
};
self.next_id = self.next_id + 1;
// Ownership: todo is moved into the collection
self.todos.push(todo);
}
fn toggle(&mut self, id: u32) {
for todo in &mut self.todos {
if todo.id == id {
todo.done = !todo.done;
}
}
}
Notice the ownership semantics: todo is
moved into the todos array – there is no
implicit copy. The toggle method borrows the array mutably
with &mut self.todos to modify items in place.
Step 4: Filter with Pattern Matching
fn visible_todos(&self) -> [&Todo] {
match self.filter {
Filter::All => &self.todos,
Filter::Active => self.todos.iter().filter(fn(t: &Todo) -> bool { !t.done }),
Filter::Completed => self.todos.iter().filter(fn(t: &Todo) -> bool { t.done }),
}
}
Pattern matching on self.filter returns the appropriate
subset. The return type [&Todo] signals that we return
borrowed references, not copies.
Step 5: Render the UI
render {
<div>
<h1>"Nectar Todo"</h1>
<div>
<input placeholder="What needs to be done?" />
<button on:click={self.add_todo}>"Add"</button>
</div>
<ul>
{self.visible_todos()}
</ul>
</div>
}
}
The template renders the filtered todo list. Thanks to fine-grained reactivity, only the changed DOM nodes update when a todo is added or toggled.
Compile and Run
nectar build todo.nectar --emit-wasm
nectar dev --port 3000
Initializing a Project
For larger projects, use nectar init to create a project
with dependency management:
mkdir my-app && cd my-app
nectar init --name my-app
This creates an Nectar.toml manifest:
[package]
name = "my-app"
version = "0.1.0"
[dependencies]
Add dependencies:
nectar add ui-components --version "^1.0"
nectar install
Build the project:
nectar build src/main.nectar --emit-wasm -O2
Component Composition with Routers
Nectar supports nested routing with layout components using
<Outlet />:
component NavBar() {
render {
<nav>
<a on:click={self.go_home}>"Home"</a>
<a on:click={self.go_about}>"About"</a>
</nav>
}
fn go_home(&mut self) {
navigate("/");
}
fn go_about(&mut self) {
navigate("/about");
}
}
router AppRouter {
layout {
<div>
<NavBar />
<Outlet />
</div>
}
route "/" => Home,
route "/about" => About,
fallback => NotFound,
}
The <Outlet /> element marks where the routed page
content renders. The surrounding layout (NavBar) persists across
navigations without re-rendering.
Generics and Monomorphization
Nectar supports generic functions that are specialized at compile time:
fn max<T>(a: T, b: T) -> T where T: Ord {
if a > b { a } else { b }
}
let bigger = max(10, 20); // generates max__i32
let longer = max("abc", "xyz"); // generates max__String
The compiler creates a separate WASM function for each concrete type, eliminating runtime type dispatch.
Next Steps
Now that you have the basics, explore these resources:
- Language Reference – complete syntax and semantics for every construct
- Toolchain Reference – all CLI commands, flags, and configuration
- Runtime API Reference – every WASM import function available at runtime
- Architecture & Internals – how the compiler works under the hood
- Examples Guide – detailed walkthroughs of all example programs Last verified deploy: 2026-05-07 16:36 EDT