NexusCS

MarkoJS

Web Frameworks
MarkoJS is a compiler-based UI framework created by eBay with built-in streaming SSR, partial hydration, and an HTML-first syntax.
marko
ebay
components
streaming
ssr

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