DOM

March can target the browser via --target js. The Dom stdlib module exposes the browser’s Document Object Model as typed March functions — no JavaScript required on your side.


Project setup

Create a forge project:

forge new my_app
cd my_app

The default forge.toml works as-is. Build to JavaScript with:

forge build --target=js

The output lands at .march/build/debug/my_app.mjs.


The HTML wrapper

March compiles to an ES module. Wire it into your page with a single script tag and a call to the exported main function:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>My App</title>
</head>
<body>
  <div id="root"></div>
  <script type="module" src="my_app.mjs"></script>
</body>
</html>

Your main function runs when the module loads. Because it is a module, it runs after the DOM is parsed — no DOMContentLoaded listener needed.

You also need two runtime files alongside the .mjs:

File Where to get it
march_runtime.mjs runtime/march_runtime.mjs in the March repo
march_dom.mjs runtime/march_dom.mjs in the March repo

Copy them next to your index.html. The build script in demo_app/dom_demo/build.sh does this automatically.


Querying the DOM

Dom.find looks up an element by id and returns Option(Node):

match Dom.find("my-button") do
  None    -> ()
  Some(btn) -> Dom.set_text(btn, "Ready")
end

Dom.select takes any CSS selector:

match Dom.select(".active") do
  None    -> ()
  Some(el) -> Dom.add_class(el, "highlight")
end

Dom.select_all returns every matching element as a List(Node):

let items = Dom.select_all("li.todo")
-- items : List(Node)

Creating elements

Build a node tree with Dom.create, set it up, then attach it:

let div = Dom.create("div")
Dom.add_class(div, "card")
Dom.set_text(div, "Hello!")
Dom.set_style(div, "color", "royalblue")

match Dom.find("root") do
  None         -> ()
  Some(root) -> Dom.append(root, div)
end

Dom.text_node creates a bare text node when you want to mix text and elements without wrapping them in a container.


Tree operations

Function What it does
Dom.append(parent, child) Add child as the last child
Dom.prepend(parent, child) Add child as the first child
Dom.remove(el) Detach el from its parent
Dom.remove_child(parent, child) Remove a known direct child
Dom.clear(el) Remove all children
Dom.parent(el) Option(Node) — parent or None
Dom.children(el) List(Node) — direct element children
Dom.clone(el) Deep-copy a node

Moving a node is just remove + append — or append alone if you know the parent (the browser removes it from the old position automatically):

-- Move the last tile to the front
match Dom.find("board") do
  None -> ()
  Some(board) ->
    match Dom.last_child(board) do
      None       -> ()
      Some(tile) -> Dom.prepend(board, tile)
    end
end

Attributes, classes, and styles

-- Attributes
Dom.set_attr(el, "href", "#section-2")
Dom.set_attr(el, "aria-hidden", "true")
let href = Dom.get_attr(el, "href")  -- Option(String)

-- CSS classes
Dom.add_class(el, "active")
Dom.remove_class(el, "inactive")
Dom.toggle_class(el, "open")
let is_open = Dom.has_class(el, "open")  -- Bool

-- Inline styles
Dom.set_style(el, "background", "#3498db")
Dom.set_style(el, "transform", "scale(1.05)")
Dom.set_style(el, "transform", "")   -- empty string removes the property

Events

Dom.listen attaches a handler that receives the Event:

Dom.listen(btn, "click", fn ev ->
  Dom.prevent_default(ev)
  -- ...
)

The handler is a regular March lambda, so it can close over local variables:

fn add_item(list, label: String) : Unit do
  let li = Dom.create("li")
  Dom.set_text(li, label)
  Dom.listen(li, "click", fn _ev ->
    Dom.remove(li)
  )
  Dom.append(list, li)
end

Common events: "click", "input", "change", "keydown", "keyup", "submit", "mouseover", "mouseout", "focus", "blur".

To read back the element that fired the event:

Dom.listen(container, "click", fn ev ->
  let target = Dom.event_target(ev)
  Dom.add_class(target, "selected")
)

Dom.event_key reads the key that triggered a "keydown"/"keyup" event (e.g. "ArrowLeft", "a", " " for space):

Dom.listen(Dom.body(), "keydown", fn ev ->
  match Dom.event_key(ev) do
    "ArrowLeft"  -> move_left()
    "ArrowRight" -> move_right()
    _            -> ()
  end
)

Form inputs

match Dom.find("search-box") do
  None -> ()
  Some(input) ->
    Dom.listen(input, "input", fn _ev ->
      let query = Dom.get_value(input)
      run_search(query)
    )
end

Dom.set_value writes back to the input’s .value property.


Timers and animation

-- Run once after 500 ms
Dom.set_timeout(500, fn _ ->
  Dom.set_text(status, "Done!")
)

-- Run every 1 s
Dom.set_interval(1000, fn _ ->
  tick()
)

-- Smooth animation at 60 fps
fn animate(el) : Unit do
  Dom.on_frame(fn _ ->
    let cur = Dom.get_style(el, "opacity")
    let next = float_to_string(string_to_float(cur) -. 0.02)
    Dom.set_style(el, "opacity", next)
    if string_to_float(next) >. 0.0 do animate(el) end
  )
end

Complete example

A row of colored tiles that move when clicked:

lib/my_app.march

mod MyApp do

  fn move_to_end(tile) : Unit do
    match Dom.parent(tile) do
      None    -> ()
      Some(p) ->
        Dom.remove(tile)
        Dom.append(p, tile)
    end
  end

  fn add_tile(board, label: String, color: String) : Unit do
    let tile = Dom.create("div")
    Dom.add_class(tile, "tile")
    Dom.set_style(tile, "background", color)
    Dom.set_text(tile, label)
    Dom.listen(tile, "click", fn _ev ->
      let _ = move_to_end(tile)
    )
    Dom.append(board, tile)
  end

  fn main() : Unit do
    match Dom.find("board") do
      None -> ()
      Some(board) ->
        add_tile(board, "A", "#e74c3c")
        add_tile(board, "B", "#3498db")
        add_tile(board, "C", "#2ecc71")
        add_tile(board, "D", "#f39c12")
        add_tile(board, "E", "#9b59b6")
    end
  end

end

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Tiles</title>
  <style>
    #board { display: flex; gap: 1rem; padding: 1rem; }
    .tile  { width: 64px; height: 64px; border-radius: 8px;
             display: flex; align-items: center; justify-content: center;
             font-size: 1.5rem; color: white; cursor: pointer; }
  </style>
</head>
<body>
  <div id="board"></div>
  <script type="module" src="my_app.mjs"></script>
</body>
</html>

Build and open:

bash build.sh
open index.html

The full runnable version lives in demo_app/dom_demo/.


Tips

Don’t annotate Node directly. Resource types from an external module can’t be spelled as Dom.Node in type annotations yet — the type checker sees them as the internal name Node. Let inference handle them:

-- ✓ let the type be inferred
fn highlight(el) do
  Dom.add_class(el, "selected")
end

-- ✗ causes a type error
fn highlight(el: Dom.Node) do ...

main is the entry point. The emitted .mjs calls <ModuleName>_main() at module load time. Name your entry function main and it runs automatically.

All DOM calls are JS-only. Dom functions panic at runtime if the binary was compiled without --target js. Keep DOM logic in files that are never imported by a native build.

march — interactive
Click run on any snippet to try it here.
march>