Back to Blog
guidebasicstutorial

OlumJS in 8 Steps: The Quick Guide

ES

Eissa Saber

Creator of OlumJS

Aug 25, 2026

3 min read

1

Start here

A component is one .html file: <script> for logic, <style> for scoped CSS, and markup. Only the markup is required.

The file name is the component name. Counter.html becomes <Counter />. Component tags start with a capital letter.

One rule for the syntax: when, each, key, on*, and html hold real JavaScript. Everything else is a string. Use {expr} for the moving parts.

1. User Interface

Put reactive data in a top-level const called state. Write to it and the view updates. There is no setter and no render call.

{state.taps} prints the value, onclick writes to it. That is the whole loop.

<script>
  const state = { taps: 0 };
</script>

<button onclick="state.taps++">{state.taps}</button>

2. Props

Props pass data down. The parent keeps the value and passes a function so the child can ask for a change.

A function prop must be a name. onChange="{setScore}" works, an inline arrow doesn't.

<!-- Parent -->
<script>
  import Child from "./Child";

  const state = { score: 0 };
  const setScore = (n) => (state.score = n);
</script>

<Child title="Score" value="{state.score}" onChange="{setScore}" />

Call props() once at the top of <script> and destructure. The names stay live, and step = 1 is the default when the parent leaves that prop out.

Props are read-only. The child can't write to value, so it calls onChange instead.

<!-- Child -->
<script>
  import { props } from "olum";

  const { title, value, onChange, step = 1 } = props();
  const inc = () => onChange(value + step);
</script>

<p>{title}: {value}</p>
<button onclick="inc()">+{step}</button>

3. Slots

Anything between a component's tags becomes a prop called children. The empty <Box /> passes nothing.

<!-- Parent.html -->
<script>
  import Box from "./Box";
</script>

<Box>
  <h3>Title</h3>
  <p>Any markup you like.</p>
</Box>

<Box />   <!-- shows the fallback -->

The child drops children wherever it wants. <if> with <else> gives a fallback when the parent passed nothing.

<!-- Box.html -->
<script>
  import { props } from "olum";
  const { children } = props();
</script>

<div class="box">
  <if when="children">{children}</if>
  <else><em>Nothing inside yet.</em></else>
</div>

4. onMount and cleanup

onMount runs once, when the component enters the page. It can be async, so you fetch right inside it.

Return a function to clean up. It runs when the component leaves — here it stops the interval.

<script>
  import { onMount } from "olum";

  const state = { users: [], seconds: 0 };

  onMount(async () => {
    const res = await fetch("https://jsonplaceholder.typicode.com/users");
    const json = await res.json()
    state.users = json;

    const id = setInterval(() => state.seconds++, 1000);

    return () => clearInterval(id);   // cleanup
  });
</script>

<p>{state.users.length} users · {state.seconds}s</p>

5. Watchers

A watcher runs whenever a value changes. Declare a top-level const called watcher with one method per key. Each method gets the old value and the new one.

It only fires on top-level keys. For a nested change, assign a fresh object to the key.

<script>
  const state = { query: "" };

  const watcher = {
    query(old, next) {
      console.log(old, next)
    },
  };
</script>

<input value="{state.query}" oninput="(e) => state.query = e.target.value" />

6. If, else-if, else

when holds a JS expression. False removes the content from the page, so a component inside unmounts and its cleanup runs.

To hide something without unmounting it, use <show when="..."> instead.

<script>
  const state = { status: "loading" };
  const set = (s) => (state.status = s);
</script>

<if when="state.status === 'loading'"><p>Loading…</p></if>
<else-if when="state.status === 'error'"><p>Something broke.</p></else-if>
<else><p>All good.</p></else>

<button onclick="set('done')">Finish</button>

7. Loops

each takes item of array. Add key so every row keeps its own DOM when the list reorders or shrinks.

Two more forms exist: key in object, and n of 3 for a plain count.

<script>
  const state = {
    todos: [
      { id: 1, text: "Read this guide"},
      { id: 2, text: "Build something" },
    ],
  };
</script>

<for each="todo of state.todos" key="todo.id">
  <li>
    {todo.text}
  </li>
</for>

8. Shared state: the store

state belongs to one component; a store belongs to the whole app.

Write it once in a plain .js file and import it anywhere. There's no provider to set up.

// cart.js
import { store } from "olum";

export const cart = store({ items: ["Lantern", "Tent", "Hatchet"] });

Reading the store during render subscribes the component. It re-renders on every change, and there's nothing to clean up.

The buttons write to the store directly — any component can.

<!-- Basket -->
<script>
  import { cart } from "./cart.js";
</script>

<p>{cart.items.length} items</p>

<for each="name of cart.items" key="name"><li>{name}</li></for>

<button onclick="cart.items.push('Apple')">Add apple</button>
<button onclick="cart.items= []">Empty</button>

Next

Three rules to remember. Code lives in when, each, key, on*, and html; everything else is a string with {expr}. A function prop is always a name, never an inline arrow. Add key to any list that can reorder or shrink.

The docs cover routing, forms, transitions, and scoped CSS. The playground runs all of this with no install.

ES

Eissa Saber

Creator of OlumJS