Getting started
Introduction
MarkoJS compiles templates into optimized JavaScript with built-in streaming SSR since 2014. Marko 6 introduces the Tags API for fine-grained reactivity.
- HTML-first template syntax
- Streaming server rendering
- Partial/granular hydration
- Two syntax modes (HTML & Concise)
Installation
# Create new Marko app
npm init marko -- -t basic
cd ./my-marko-application
npm run dev
Vite Setup
npm install marko@next
npm install -D vite @marko/vite
// vite.config.js
import { defineConfig } from "vite";
import marko from "@marko/vite";
export default defineConfig({
plugins: [marko()],
});
First Component
<let/count=0>
<const/double=count * 2>
<button onClick() { count++ }>
Count: ${count} (double: ${double})
</button>
<style>
button { font-size: 1.2rem; }
</style>
Syntax basics
HTML Mode
<div class="container">
<h1>Hello ${name}</h1>
<if=loggedIn>
<p>Welcome back!</p>
</if>
<else>
<p>Please log in</p>
</else>
</div>
File starts in concise mode, switches to HTML when < appears.
Concise Mode
div.container
h1 -- Hello ${name}
if=loggedIn
p -- Welcome back!
else
p -- Please log in
Indent-based, no angle brackets. Use -- for text content.
Interpolation
<!-- Escaped text output -->
${expression}
<!-- Dynamic tags -->
<${"h" + level}>Dynamic heading</>
<${condition ? TagA : TagB}/>
All ${} expressions are auto-escaped by default.
Module-Level Statements
import MyTag from "<my-tag>"
export function helper() { return 42; }
static const API_URL = "/api"
server console.log("server only")
client console.log("browser only")
static runs at build time. server/client control execution environment.
Tags & attributes
Attribute Values
<!-- Values are JS expressions -->
<tag num=1+1 date=new Date()/>
<input type="text" disabled/>
<!-- String attributes -->
<div title="hello world"/>
<div title=`template ${lit}`/>
Shorthand Syntax
<!-- id and class shorthand -->
<div#my-id/>
<div.foo.bar/>
<div#my-id.foo.bar/>
<!-- Equivalent to: -->
<div id="my-id" class="foo bar"/>
Spread & Methods
<!-- Spread attributes -->
<tag ...input/>
<tag ...{ class: "a", id: "b" }/>
<!-- Method shorthand -->
<button onClick(e) {
alert("Clicked!")
}>Click</button>
<!-- Two-way binding -->
<counter value:=count/>
:= creates two-way binding between parent and child.
Control flow
Conditionals
<if=condition>
<p>Truthy</p>
</if>
<else if=otherCondition>
<p>Other</p>
</else>
<else>
<p>Fallback</p>
</else>
Loops
<!-- Array iteration -->
<for|item, index| of=array>
<p>${index}: ${item.name}</p>
</for>
<!-- Object iteration -->
<for|key, value| in=object>
<p>${key} = ${value}</p>
</for>
<!-- Range iteration -->
<for|n| from=0 to=5 step=1>
<p>Number: ${n}</p>
</for>
|item, index| are tag parameters — scoped to the tag body.
State & reactivity
Mutable State (<let>)
<let/count=0>
<let/name="Marko">
<let/items=["a", "b", "c"]>
<button onClick() { count++ }>
${count}
</button>
<input value:=name/>
<p>Hello ${name}</p>
<let> declares reactive mutable state. Changes trigger re-renders.
Derived Values (<const>)
<let/count=0>
<const/double=count * 2>
<const/label=`Count is ${count}`>
<p>${label}</p>
<p>Double: ${double}</p>
<const> values recompute automatically when dependencies change.
Core tags
State Tags
| Tag | Purpose |
|---|---|
<let> |
Mutable reactive state |
<const> |
Derived/computed value |
<return> |
Expose value to parent |
<id> |
Generate unique ID |
<id/myId>
<label for=myId>Name</label>
<input id=myId type="text"/>
Lifecycle Tags
| Tag | Purpose |
|---|---|
<script> |
Browser-side effect |
<lifecycle> |
onMount/onUpdate/onDestroy |
<define> |
Inline reusable template |
<script>
// Runs in browser
// Re-runs when deps change
document.title = `Count: ${count}`;
</script>
<lifecycle
onMount() { console.log("mounted") }
onDestroy() { console.log("bye") }
/>
Utility Tags
| Tag | Purpose |
|---|---|
<await> |
Async data loading |
<try> |
Error boundary |
<log> |
console.log helper |
<debug> |
debugger statement |
<log=someValue/>
<!-- same as console.log(someValue) -->
<debug/>
<!-- triggers debugger statement -->
Components
Component Inputs
<!-- my-card.marko -->
<div class="card">
<h2>${input.title}</h2>
<p>${input.description}</p>
</div>
<!-- Usage -->
<my-card
title="Hello"
description="World"
/>
input is available in every template — contains all passed attributes.
Content & Attribute Tags
<!-- my-layout.marko -->
<div class="layout">
<header><${input.header}/></header>
<main><${input.content}/></main>
<footer><${input.footer}/></footer>
</div>
<!-- Usage -->
<my-layout>
<@header>Site Title</@header>
<@footer>Copyright 2026</@footer>
Body content here
</my-layout>
<@name> creates attribute tags accessible via input.name.
Tag Variables
<!-- counter.marko (child) -->
<let/count=0>
<return={count, increment() { count++ }}>
<!-- Usage (parent) -->
<counter/result/>
<p>Count: ${result.count}</p>
<button onClick() { result.increment() }>
+1
</button>
<tag/variable/> captures the tag's return value.
Styling
Basic & CSS Modules
<!-- Global styles -->
<style>
.container { max-width: 960px; }
</style>
<!-- CSS Modules (scoped) -->
<style/styles>
.foo { color: red; }
.bar { font-size: 1.2rem; }
</style>
<div class=styles.foo>Scoped red text</div>
Enhanced Class & Style
<!-- Object class syntax -->
<div class={
active: isActive,
disabled: isDisabled,
"my-class": true
}/>
<!-- Object style syntax -->
<div style={
display: "block",
"margin-right": 16,
color: textColor
}/>
Preprocessors
<!-- SCSS -->
<style.scss>
.card {
&__title { font-weight: bold; }
&__body { padding: 1rem; }
}
</style>
<!-- LESS -->
<style.less>
.card { .mixin(); }
</style>
Use <style.ext> to specify any CSS preprocessor.
Async & streaming
Await with Error Boundary
<try>
<@placeholder>
<p>Loading...</p>
</@placeholder>
<@catch|err|>
<p>Error: ${err.message}</p>
</@catch>
<await|data|=fetchData()>
<p>${data.title}</p>
<ul>
<for|item| of=data.items>
<li>${item}</li>
</for>
</ul>
</await>
</try>
Streaming SSR sends the placeholder first, then replaces with data when ready.
Template Variables
<!-- input: props from parent -->
<p>${input.name}</p>
<!-- $signal: AbortSignal for cleanup -->
<script>
const ctrl = new AbortController();
fetch("/api", { signal: $signal });
</script>
<!-- $global: render-wide globals -->
<p>Locale: ${$global.locale}</p>
| Variable | Scope | Purpose |
|---|---|---|
input |
Template | Props from parent |
$signal |
Script | Auto-cleanup AbortSignal |
$global |
Template | Render globals |
Marko Run routing
Route Files
Routes live in src/routes/:
| File | Purpose |
|---|---|
+page.marko |
Page component |
+layout.marko |
Layout wrapper |
+handler.ts |
Route handler (GET, POST) |
+middleware.ts |
Middleware |
src/routes/
+page.marko # /
+layout.marko # Root layout
about/
+page.marko # /about
blog/
+page.marko # /blog
$slug/
+page.marko # /blog/:slug
Path Types
| Pattern | Example | Matches |
|---|---|---|
foo/ |
about/ |
/about (static) |
$param/ |
$id/ |
/:id (dynamic) |
$$rest/ |
$$path/ |
/*path (catch-all) |
_name/ |
_auth/ |
pathless (layout group) |
// +handler.ts
export function GET(context) {
return new Response("Hello");
}
export function POST(context) {
const body = await context.request.json();
return Response.json({ ok: true });
}
Event handling
Method Shorthand
<!-- Inline handler -->
<button onClick(e) {
e.preventDefault();
alert("Clicked!");
}>Click me</button>
<!-- Multiple events -->
<input
onFocus() { console.log("focused") }
onBlur() { console.log("blurred") }
onInput(e) { value = e.target.value }
/>
Alternative Syntax
<!-- on-* syntax (Marko 5 style) -->
<button on-click() {
alert("Hi!")
}>Click</button>
<!-- With state -->
<let/text="">
<input
type="text"
value:=text
onInput(e) { text = e.target.value }
/>
<p>You typed: ${text}</p>
Controllable native tags
Form Elements
<!-- Text input -->
<let/text="">
<input type="text" value:=text/>
<p>Value: ${text}</p>
<!-- Checkbox -->
<let/checked=false>
<input type="checkbox" checked:=checked/>
<p>Checked: ${checked}</p>
<!-- Select -->
<let/selected="en">
<select value:=selected>
<option value="en">English</option>
<option value="es">Spanish</option>
</select>
Inline Templates (<define>)
<define/Greeting|{ name }|>
<p>Hello, ${name}!</p>
</define>
<Greeting name="Alice"/>
<Greeting name="Bob"/>
<!-- Conditional template -->
<define/Badge|{ type }|>
<span class=`badge badge-${type}`>
${type}
</span>
</define>
<Badge type="success"/>
<Badge type="error"/>
Class components (legacy)
Marko 5 Syntax
class {
onCreate() {
this.state = { count: 0 };
}
increment() {
this.state.count++;
}
}
<p>Count: ${state.count}</p>
<button on-click('increment')>
+1
</button>
Class components are Marko 5 legacy — prefer Tags API (<let>, <const>) in Marko 6.
Lifecycle Methods
| Method | When |
|---|---|
onCreate(input) |
Component created |
onInput(input) |
New input received |
onRender(out) |
Before DOM update |
onMount() |
First DOM insert |
onUpdate() |
After DOM re-render |
onDestroy() |
Component removed |
class {
onCreate() {
this.state = { time: Date.now() };
}
onMount() {
this.interval = setInterval(() => {
this.state.time = Date.now();
}, 1000);
}
onDestroy() {
clearInterval(this.interval);
}
}
MarkoJS vs React
Key Differences
| Feature | MarkoJS | React |
|---|---|---|
| Rendering | Compiler-based | Virtual DOM |
| SSR | Built-in streaming (2014) | React 18+ |
| Hydration | Partial/granular | Full page |
| State | <let>, <const> |
useState, useMemo |
| Effects | <script>, <lifecycle> |
useEffect |
| Styling | Built-in <style> |
External |
| Two-way binding | := shorthand |
Manual |
| File extension | .marko |
.jsx / .tsx |
Equivalent Patterns
<!-- MarkoJS -->
<let/count=0>
<const/double=count * 2>
<button onClick() { count++ }>
${count} (${double})
</button>
// React equivalent
function Counter() {
const [count, setCount] = useState(0);
const double = useMemo(() => count * 2, [count]);
return (
<button onClick={() => setCount((c) => c + 1)}>
{count} ({double})
</button>
);
}
Also see
- MarkoJS official docs — comprehensive reference
- MarkoJS GitHub — source and issues
- Marko Run — file-based routing framework
- Marko Tags API reference — all built-in tags
- eBay engineering blog — Marko case studies