ArcMoon 1.1.0

Start

Language

Reference

Nothing matches “”.

↑↓ move Enter open Esc close

Language

Components

[import], props, slots and props that fall through.

What a component is

A component is an .arcm file you use like a block. Write it once, use it anywhere, with different props each time:

Button.arcm
${ const { label = "Click me", kind = "primary" } = ArcMoon.props(); }$
[button = class: ${ `btn ${kind}` }$]${ label }$[end]
index.arcm
[import = Button: "./Button.arcm" !]
[Button = label: "Save" !]
[Button = label: "Cancel", kind: "ghost" !]
[Button!]
HTML ArcMoon builds
<button class="btn primary">Save</button>
<button class="btn ghost">Cancel</button>
<button class="btn primary">Click me</button>
  • A component is a normal .arcm file: markup, ${ }$, runtime ${ }$ and [style] all work in it.
  • Its output takes the place of the block that uses it. The HTML has no trace of the component itself.

Importing

[import] loads a component file and gives it a name you can use as a block in this file:

[import = Card: "./Card.arcm" !]
#        ↑ name  ↑ path to the file

[Card!]   # use it like a block, by that name

Paths

A path starts from the folder of the file that has the [import]. With this project:

my-site
pages/
  index.arcm
  blog/
    post.arcm
src/
  components/
    Card.arcm
    Badge.arcm
# in pages/index.arcm
[import = Card: "../src/components/Card.arcm" !]

# in pages/blog/post.arcm (one folder deeper, so one more ../)
[import = Card: "../../src/components/Card.arcm" !]

# in src/components/Card.arcm (same folder)
[import = Badge: "./Badge.arcm" !]
  • ./ is the same folder, ../ the folder above.
  • Write the .arcm ending: "./Card" is an error.
  • A path starting with / is a full path on your computer, not the project folder. Use a relative path or an alias instead.

Rules

  • The name is yours. Start it with a capital letter, so it reads as a component: [Card], not [card].
  • At the top. Imports come before any markup.
  • Self-closing. Always end with !.
  • Only .arcm files. For .js or .json, use JavaScript import inside ${ }$.
  • One name, one component. The same name can't be imported twice. The same file can be imported under two names.
  • Per file. Each file imports what it uses. A component used inside another component needs its own [import], even if the page imported it.
✗ [import] must be at the top of the file
✗ [import] must be self-closing: [import = Name: "./file.arcm" !]
✗ [import] only loads .arcm files, got "./Card"
✗ "Card" is imported twice

Components from npm packages

A path that doesn't start with . or / loads from node_modules, like JavaScript imports:

[import = Button: "arcm-ui/Button.arcm" !]   # node_modules/arcm-ui/Button.arcm

Code from a package can run on your computer when you build, so ArcMoon asks you to trust it first. Review it, then trust it once:

⚠ Template "arcm-ui" is not trusted. It would:
    (no code)
  Review it, then run "arcmoon trust arcm-ui".
arcm trust pages/index.arcm   # trusts the packages this page uses, saved in arcmoon.trust.json

Import aliases

Deep folders make long paths, and the path changes with every folder level. An alias is a short name for a folder, so the same path works from any file. Set it in arcmoon.config.js:

arcmoon.config.js
export default {
  importAliases: {
    "@": "./src",
    "~ui": "./src/components"
  }
};

Now every file can use the same path, however deep it is:

# in pages/index.arcm and in pages/blog/post.arcm alike
[import = Card: "@/components/Card.arcm" !]   # → ./src/components/Card.arcm
[import = Badge: "~ui/Badge.arcm" !]          # → ./src/components/Badge.arcm
  • An alias is replaced at the start of the path: @/components/Card.arcm becomes ./src/components/Card.arcm. Write the / after it.
  • Targets start from the project folder, where arcmoon.config.js is. Run arcm in that folder: it only reads the config from where you run it.
  • Any name works; @ is common. You can have as many aliases as you like.
  • From JavaScript, pass the same option: new ArcMoon({ src, importAliases: { "@": "./src" } }).

If arcm can't find the config, the alias isn't known, and the path is read as a package:

✗ can't read "@/components/Card.arcm" (/my-site/pages/node_modules/@/components/Card.arcm): ENOENT: no such file or directory

Run arcm from the project folder, or check the alias name.

Props

A component reads the props it was given with ArcMoon.props(), at the top of its file. Give each one a default for when it isn't passed:

Profile.arcm
${
  const { name, role = "Member", skills = [] } = ArcMoon.props();
}$
[article = class: "profile"]
  [h3]${ name }$[end]
  [p]${ role }$[end]
  [ul]
    [for-each = ${ skills }$, as: "skill"][li]${ skill }$[end][end]
  [end]
[end]
index.arcm
[import = Profile: "./Profile.arcm" !]
[Profile = name: "Adam", role: "Engineer", skills: ${ ["JavaScript", "ArcMoon"] }$ !]
[Profile = name: "Elmi" !]
HTML ArcMoon builds
<article class="profile">
  <h3>Adam</h3>
  <p>Engineer</p>
  <ul>
    <li>JavaScript</li>
    <li>ArcMoon</li>
  </ul>
</article>
<article class="profile">
  <h3>Elmi</h3>
  <p>Member</p>
  <ul></ul>
</article>
  • Any value works: text, numbers, true / false, and arrays or objects through ${ }$ (skills above).
  • role = "Member" is a default: Elmi didn't pass a role, so he gets Member.
  • Values without a name come by position: [Tag = "new", 3 !] gives ArcMoon.props()[0] = "new" and [1] = 3.
  • Call ArcMoon.props() at the top of the file. Inside an element it returns that element's props instead.

Props that pass through

Props the component doesn't read go onto its outer element. So id, data-, aria- and css. props work on any component, with no extra code:

Card.arcm
${ const { title } = ArcMoon.props(); }$
[div = class: "card"]
  [h3]${ title }$[end]
[end]
index.arcm
[import = Card: "./Card.arcm" !]
[Card = title: "Hello", id: "intro", data-kind: "news", aria-label: "Intro card", css.margin: "8px" !]
HTML ArcMoon builds
<div
  class="card"
  id="intro"
  data-kind="news"
  aria-label="Intro card"
  style="margin: 8px"
>
  <h3>Hello</h3>
</div>
  • title is read by the component, so it stays data. The others are not, so they land on the <div>.
  • A prop counts as read when the component names it: const { title } = ArcMoon.props(). If it keeps the whole object (const p = ArcMoon.props()), nothing passes through.
  • They go to the first element. A passed class is added to the element's own: [Card = class: "big" !] gives class="card big". A passed style comes after the element's own, so its rules win.
  • Any other attribute the element already has keeps its own value, and ArcMoon warns: [Card] sets id on its <div> already, so the id passed to it is ignored.

Slots

Props pass small values. To pass a piece of markup, give the component a body. The component decides where that body goes with [slot!]: a hole that the caller fills.

Box.arcm
[div = class: "box"]
  [slot!]
[end]
index.arcm
[import = Box: "./Box.arcm" !]
[Box]
  [p]Hello from the page.[end]
[end:Box]
HTML ArcMoon builds
<div class="box">
  <p>Hello from the page.</p>
</div>
  • 1. The page uses [Box] with a body: the [p].
  • 2. ArcMoon builds Box.arcm, and where it finds [slot!], it puts that body.
  • 3. The result is the box with the page's paragraph inside.

Anything can go in the slot

The body is normal markup: elements, other components, ${ }$, [for-each], runtime code:

Panel.arcm
${ const { title } = ArcMoon.props(); }$
[section = class: "panel"]
  [h2]${ title }$[end]
  [slot]Nothing here yet.[end]
[end]
index.arcm
[import = Panel: "./Panel.arcm" !]
${ const tasks = ["Write docs", "Ship 1.0"]; }$
[Panel = title: "Tasks"]
  [ul]
    [for-each = ${ tasks }$, as: "task"][li]${ task }$[end][end]
  [end]
[end:Panel]
HTML ArcMoon builds
<section class="panel">
  <h2>Tasks</h2>
  <ul>
    <li>Write docs</li>
    <li>Ship 1.0</li>
  </ul>
</section>

Fallback content

[slot]…[end] holds content to show when the caller passes no body. Panel.arcm above has one:

index.arcm
[import = Panel: "./Panel.arcm" !]
[Panel = title: "With a body"][p]Here it is.[end][end:Panel]
[Panel = title: "Self-closing" !]
[Panel = title: "Empty body"][end:Panel]
HTML ArcMoon builds
<section class="panel">
  <h2>With a body</h2>
  <p>Here it is.</p>
</section>
<section class="panel">
  <h2>Self-closing</h2>
  Nothing here yet.
</section>
<section class="panel">
  <h2>Empty body</h2>
  Nothing here yet.
</section>
  • A body shows the body. Self-closing ([Panel !]) or an empty body ([Panel][end]) shows the fallback.
  • A body with only a space is not empty: it shows the space, not the fallback.

The body belongs to the caller

The body is written in the page, so it uses the page's variables and the page's styles, not the component's:

Scoped.arcm
${ const name = "the component"; }$
[div = class: "box"][slot!][end]
[style]
  p { color: crimson; }
[end]
index.arcm
[import = Scoped: "./Scoped.arcm" !]
${ const name = "the page"; }$
[Scoped][p]Written by ${ name }$.[end][end:Scoped]
HTML ArcMoon builds
<style>p[data-a-radaa7]{color:#dc143c}</style><div class="box" data-a-radaa7><p>Written by the page.</p></div>
  • ${ name }$ in the body reads the page's name, even though the component has its own.
  • The component's p { color: crimson; } doesn't reach the page's <p>: it has no data-a-… attribute. Style it from the page.

Where the slot goes

A slot can go anywhere in the component, even in the middle of text:

Quote.arcm
[p = class: "quote"]“[slot!]”[end]
index.arcm
[import = Quote: "./Quote.arcm" !]
[Quote]Simple is better.[end]
HTML ArcMoon builds
<p class="quote">“Simple is better.”</p>

Each [slot!] writes the body again, so two slots show it twice.

Passing a slot on

A component can put its own slot inside another component's body. The page's content travels down both:

Layout.arcm
[import = Panel: "./Panel.arcm" !]
[main]
  [Panel = title: "Page"][slot!][end:Panel]
[end]
index.arcm
[import = Layout: "./Layout.arcm" !]
[Layout][p]My page content.[end][end:Layout]
HTML ArcMoon builds
<main>
  <section class="panel">
    <h2>Page</h2>
    <p>My page content.</p>
  </section>
</main>

More than one area

There is one kind of slot: no named slots. For a second area, like a header, pass it as a prop. Text works, and { raw } passes HTML:

Card.arcm
${ const { header } = ArcMoon.props(); }$
[article = class: "card"]
  [header]${ header }$[end]
  [slot!]
[end]
index.arcm
[import = Card: "./Card.arcm" !]
[Card = header: "Plain title"][p]Body text.[end][end:Card]
[Card = header: ${ { raw: "<b>Bold</b> title" } }$][p]Body text.[end][end:Card]
HTML ArcMoon builds
<article class="card">
  <header>Plain title</header>
  <p>Body text.</p>
</article>
<article class="card">
  <header><b>Bold</b> title</header>
  <p>Body text.</p>
</article>

For bigger areas, split the component: a Card whose body holds a CardHeader and a CardBody, each with its own slot.

Layouts

A layout is a component that holds the whole page frame. Every page uses it and puts its own content in the slot:

Layout.arcm
${ const { title = "My site" } = ArcMoon.props(); }$
[doctype!]
[html = lang: "en"]
  [head][title]${ title }$[end][end]
  [body]
    [header]My site[end]
    [main][slot!][end]
    [footer]© 2026[end]
  [end]
[end]
about.arcm
[import = Layout: "./Layout.arcm" !]
[Layout = title: "About"]
  [h1]About us[end]
  [p]We build pages with ArcMoon.[end]
[end:Layout]
HTML ArcMoon builds
<!doctype html>
<html lang="en">
  <head>
    <title>About</title>
  </head>
  <body>
    <header>My site</header>
    <main>
      <h1>About us</h1>
      <p>We build pages with ArcMoon.</p>
    </main>
    <footer>© 2026</footer>
  </body>
</html>

Change the header once in Layout.arcm, and every page gets it on the next build.

Styles

A [style] in a component only styles that component's own elements. The same class on the page is not touched:

Note.arcm
[p = class: "note"]Inside the component[end]
[style]
  .note { color: crimson; }
[end]
index.arcm
[import = Note: "./Note.arcm" !]
[p = class: "note"]On the page[end]
[Note!]
HTML ArcMoon builds
<style>.note[data-a-1aqzgr0]{color:#dc143c}</style><p class="note">On the page</p>
<p class="note" data-a-1aqzgr0>Inside the component</p>
  • ArcMoon adds an attribute like data-a-1aqzgr0 to the component's elements and its CSS, so the rules only match there.
  • The CSS is written once, however many times the component is used. More in Styles.

Runtime code

Each use of a component runs its own copy of its runtime code, with its own signals and refs:

Counter.arcm
${ const { start = 0 } = ArcMoon.props(); export const initial = start; }$
runtime ${
  import { signal } from "arcmoon/reactive";
  const count = signal(initial);
}$
[button = onclick: runtime ${ () => count(count() + 1) }$]runtime ${ count() }$[end]
index.arcm
[import = Counter: "./Counter.arcm" !]
[Counter = start: 5 !]    # counts from 5
[Counter = start: 10 !]   # counts from 10, on its own
  • Props reach runtime code through export: read them in ${ }$, export what the browser needs.
  • A page's defineRef() also finds refs inside the components it uses, so the page can reach a component's button.
  • A component can't see its page's runtime variables. Pass values as props instead.

Mistakes

Each message below stops the build (✗) or warns (⚠). Here is what causes it and how to fix it.

A path that points nowhere

✗ can't read "./Card.arcm" (/my-site/pages/Card.arcm): ENOENT: no such file or directory

The path is relative to the file that has the [import], not to where you run arcm build. The message shows the full path ArcMoon tried:

# pages/index.arcm, with Card.arcm in src/components/

# ✗ looks for pages/Card.arcm
[import = Card: "./Card.arcm" !]

# ✓ from pages/, go up one folder
[import = Card: "../src/components/Card.arcm" !]

# ✓ or with the "@" alias for ./src
[import = Card: "@/components/Card.arcm" !]

Two components that import each other

✗ circular import: Header.arcm → Menu.arcm → Header.arcm

A component would contain itself forever. Move the part both need into a third component, and import it from both:

# ✗ Header.arcm uses Menu, and Menu.arcm uses Header for its logo
# Header.arcm
[import = Menu: "./Menu.arcm" !]
[header][Menu!][end]
# Menu.arcm
[import = Header: "./Header.arcm" !]
[nav][Header!][end]

# ✓ the logo gets its own component
# Logo.arcm
[a = href: "/"]My site[end]
# Header.arcm
[import = Logo: "./Logo.arcm" !]
[import = Menu: "./Menu.arcm" !]
[header][Logo!][Menu!][end]
# Menu.arcm
[import = Logo: "./Logo.arcm" !]
[nav][Logo!][end]

A prop that wasn't passed

⚠ Card.arcm:2:7  ${ title }$ is undefined (prop "title" wasn't passed)

The component reads title, but the caller didn't give one, so it prints nothing. Pass it, or give it a default:

# Card.arcm
# ✗ empty when no title is passed
${ const { title } = ArcMoon.props(); }$
[h2]${ title }$[end]

# ✓ a default for when it is missing
${ const { title = "Untitled" } = ArcMoon.props(); }$
[h2]${ title }$[end]

Reading props inside an element

⚠ Card.arcm:1:7  ArcMoon.props() inside [h2] returns the [h2]'s props, which have no "title"; for the component's props, call it at the top of the file

Inside an element, ArcMoon.props() gives that element's own props. Read the component's props once, at the top:

# Card.arcm
# ✗ the [h2]'s props, not the component's
[h2]${ ArcMoon.props().title }$[end]

# ✓ read at the top, use below
${ const { title } = ArcMoon.props(); }$
[h2]${ title }$[end]

A component that isn't imported

⚠ page.arcm:1:1  [Card] is not imported; it is written as <card>. Did you forget [import = Card: "./Card.arcm" !]?

Without an [import], ArcMoon doesn't know [Card] is a component, so it writes an unknown <card> tag. Import it in every file that uses it, even if the page already did:

# ✗ no import in this file
[Card = title: "Hi" !]

# ✓ right
[import = Card: "./Card.arcm" !]
[Card = title: "Hi" !]