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:
${ const { label = "Click me", kind = "primary" } = ArcMoon.props(); }$
[button = class: ${ `btn ${kind}` }$]${ label }$[end][import = Button: "./Button.arcm" !]
[Button = label: "Save" !]
[Button = label: "Cancel", kind: "ghost" !]
[Button!]<button class="btn primary">Save</button>
<button class="btn ghost">Cancel</button>
<button class="btn primary">Click me</button>
- A component is a normal
.arcmfile: 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 namePaths
A path starts from the folder of the file that has the [import]. With this project:
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
.arcmending:"./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
.arcmfiles. For.jsor.json, use JavaScriptimportinside${ }$. - 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 twiceComponents 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.arcmCode 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.jsonImport 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:
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.arcmbecomes./src/components/Card.arcm. Write the/after it. - Targets start from the project folder, where
arcmoon.config.jsis. Runarcmin 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 directoryRun 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:
${
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][import = Profile: "./Profile.arcm" !]
[Profile = name: "Adam", role: "Engineer", skills: ${ ["JavaScript", "ArcMoon"] }$ !]
[Profile = name: "Elmi" !]<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${ }$(skillsabove). role = "Member"is a default: Elmi didn't pass a role, so he getsMember.- Values without a name come by position:
[Tag = "new", 3 !]givesArcMoon.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:
${ const { title } = ArcMoon.props(); }$
[div = class: "card"]
[h3]${ title }$[end]
[end][import = Card: "./Card.arcm" !]
[Card = title: "Hello", id: "intro", data-kind: "news", aria-label: "Intro card", css.margin: "8px" !]<div
class="card"
id="intro"
data-kind="news"
aria-label="Intro card"
style="margin: 8px"
>
<h3>Hello</h3>
</div>
titleis 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
classis added to the element's own:[Card = class: "big" !]givesclass="card big". A passedstylecomes 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.
[div = class: "box"]
[slot!]
[end][import = Box: "./Box.arcm" !]
[Box]
[p]Hello from the page.[end]
[end:Box]<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:
${ const { title } = ArcMoon.props(); }$
[section = class: "panel"]
[h2]${ title }$[end]
[slot]Nothing here yet.[end]
[end][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]<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:
[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]<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:
${ const name = "the component"; }$
[div = class: "box"][slot!][end]
[style]
p { color: crimson; }
[end][import = Scoped: "./Scoped.arcm" !]
${ const name = "the page"; }$
[Scoped][p]Written by ${ name }$.[end][end:Scoped]<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'sname, even though the component has its own.- The component's
p { color: crimson; }doesn't reach the page's<p>: it has nodata-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:
[p = class: "quote"]“[slot!]”[end][import = Quote: "./Quote.arcm" !]
[Quote]Simple is better.[end]<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:
[import = Panel: "./Panel.arcm" !]
[main]
[Panel = title: "Page"][slot!][end:Panel]
[end][import = Layout: "./Layout.arcm" !]
[Layout][p]My page content.[end][end:Layout]<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:
${ const { header } = ArcMoon.props(); }$
[article = class: "card"]
[header]${ header }$[end]
[slot!]
[end][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]<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:
${ 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][import = Layout: "./Layout.arcm" !]
[Layout = title: "About"]
[h1]About us[end]
[p]We build pages with ArcMoon.[end]
[end:Layout]<!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:
[p = class: "note"]Inside the component[end]
[style]
.note { color: crimson; }
[end][import = Note: "./Note.arcm" !]
[p = class: "note"]On the page[end]
[Note!]<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-1aqzgr0to 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:
${ 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][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 directoryThe 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.arcmA 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 fileInside 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" !]