Language Reference

Marko is a superset of well-formed HTML.

The language makes HTML more strict while extending it with control flow and reactive data bindings. It does this by meshing JavaScript syntax features with HTML and introducing a few new syntaxes of its own. Most HTML is valid Marko but there are some important deviations.

Syntax Legend

Note

Jump to the section for a syntax by clicking on it. The legend is not comprehensive, for more see:

Template Variables

Within Marko templates a few variables are automatically made available.

input

A JavaScript object globally available in every template that gives access to the attributes it was provided from a custom tag or the data passed in through the top level api.

$signal

An AbortSignal available in all JavaScript statements and expressions.

It is aborted when

  1. The expression is invalidated
  2. The template or tag content is removed from the DOM

This is primarily to handle cleaning up side effects.

Tip

Many built-in APIs like addEventListener() include the option to pass a signal for cleanup.

<script>
  document.addEventListener(
    "resize",
    () => {
      // this function will be automatically cleaned up
    },
    {
      signal: $signal,
    },
  );
</script>
script
  --
  document.addEventListener(
    "resize",
    () => {
      // this function will be automatically cleaned up
    },
    {
      signal: $signal,
    },
  );
  --

$global

Gives access the "render globals" provided through the top level api.

Statements

Marko supports a few module scoped top level statements.

import

JavaScript import statements are allowed at the root of the template.

import sum from "sum";

<div data-number=sum(1, 2)/>
import sum from "sum";

div data-number=sum(1, 2)

Note

This syntax is a shorthand for static import. For server and client specific imports, you can use server and client statements.

Tag import shorthand

Custom tags may be referenced using angle brackets in the from of the import, which will use Marko's custom tag discovery logic.

import MyTag from "<my-tag>";

<MyTag/>
import MyTag from "<my-tag>";

MyTag

export

JavaScript export statements are allowed at the root of the template.

export function getAnswer() {
  return 42;
}

<div>${getAnswer()}</div>
export function getAnswer() {
  return 42;
}

div -- ${getAnswer()}

static

Statements prefixed with static allow running JavaScript expressions in module scope. The statements will run when the template loaded on the server and in the browser.

static const answer = 41;
static function getAnswer() {
  return answer + 1;
}

<div data-answer=getAnswer()/>
static const answer = 41;
static function getAnswer() {
  return answer + 1;
}

div data-answer=getAnswer()

All valid javascript statements are allowed, including functions, declarations, conditions, and blocks.

static console.log("this will be logged only ONE time");
static console.log("no matter how often the component is used");
static console.log("this will be logged only ONE time");
static console.log("no matter how often the component is used");

server and client

As an alternative to static, statements prefixed with server or client allow arbitrary module scoped JavaScript expressions that are exclusively executed when the template is loaded in a specific environment (the server or the browser).

server console.log("on the server");
client console.log("in the browser");
server console.log("on the server");
client console.log("in the browser");

All valid javascript statements are allowed, including functions, declarations, conditions, and blocks.

server import { connectToDatabase } from "./database";
server const db = connectToDatabase();

server {
  console.log("Database connection established on server");

  // Only happens ONCE, when the application loads
  // and this component is used for the first time
}
server const users = await db.query("SELECT * FROM users");
server console.log(`Found ${users.length} users in the database`);
server import { connectToDatabase } from "./database";
server const db = connectToDatabase();

server {
  console.log("Database connection established on server");

  // Only happens ONCE, when the application loads
  // and this component is used for the first time
}
server const users = await db.query("SELECT * FROM users");
server console.log(`Found ${users.length} users in the database`);

Tip

The import statement is really a shortcut for static import. This can be leveraged with server and client if you want a module to only be imported on one platform

server import "./init-db";
client import "bootstrap";
server import "./init-db";
client import "bootstrap";

Tags

Marko supports all native HTML/SVG/whatever tags and attributes. In addition to these, a set of useful core tags are provided. Each project may have its own custom tags, and third-party tags may be included through node_modules.

All of these types of tags use the same syntax:

<my-tag/>
my-tag

.marko files are automatically discovered as custom tags (no need for import).

All tags can be self closed when there is no content. This means <div/> is valid, unlike in HTML. Additionally void tags like <input> and <br> can be self closed.

In all closing tags, the tag name may be omitted.

<div>Hello World</div>
div -- Hello World

Attributes

Attribute values are JavaScript expressions:

<my-tag str="Hello"/>
<my-tag str=`Hello ${name}`/>
<my-tag num=1 + 1/>
<my-tag date=new Date()/>
<my-tag fn=(
  function myFn(param1) {
    console.log("hi");
  }
)/>
my-tag str="Hello"
my-tag str=`Hello ${name}`
my-tag num=1 + 1
my-tag date=new Date()
my-tag fn=(
  function myFn(param1) {
    console.log("hi");
  }
)

Almost all valid JavaScript expressions can be written as the attribute value. Even with <my-tag str="Hello"> the "Hello" string is a JavaScript string literal and not an html attribute string.

Attributes can be thought of as JavaScript objects in Marko which are passed to a tag.

Caution

Values cannot contain an unenclosed > since it is ambiguous. These expressions must use parentheses:

<my-tag value=(1 > 2)/>
my-tag value=(1 > 2)

Skipped Attributes

If an attribute value is null, undefined or false it will not be written to the html.

Note

Not all falsy values are skipped. 0, NaN, and "" will still be written.

Boolean Attributes

HTML boolean attributes become JavaScript booleans.

<input type="checkbox" checked>
<input type="checkbox" checked>
input type="checkbox" checked
input type="checkbox" checked

Important

ARIA enumerated attributes use strings instead of booleans, so make sure to pass a string.

// ❌ WRONG: Don't do this

<button aria-pressed=isPressed/>
// outputs <button aria-pressed=""/>

// ❌ WRONG: Don't do this

button aria-pressed=isPressed
// outputs <button aria-pressed=""/>

// 👍 Correct use of aria attributes

<button aria-pressed=isPressed && "true"/>
// outputs <button aria-pressed="true"/>

// 👍 Correct use of aria attributes

button aria-pressed=isPressed && "true"
// outputs <button aria-pressed="true"/>

Spread Attributes

Attributes may be dynamically included with the spread syntax.

<my-tag ...input foo="bar"/>
my-tag ...input foo="bar"

In this case <my-tag> would receive the attributes as an object like { ...input, foo: "bar" }.

Attributes are merged from left to right, with later spreads overriding earlier ones if there are conflicts.

Note

The value after the ... (like in JavaScript) can be any valid JavaScript expression.

Shorthand Methods

Method definitions allow for a concise way to pass functions as attributes, such as event handlers.

<button onClick(ev) {
  console.log(ev.target);
}>
  Click Me
</button>
button onClick(ev) {
  console.log(ev.target);
}
  -- Click Me

Shorthand Change Handlers (Two-Way Binding)

The change handler shorthand (:=) provides both a value for an attribute and a change handler with the attribute's name suffixed by "Change".

The value must be an Identifier or a Property Accessor.

For Identifiers, the change handler desugars to a function with an assignment.

<counter value:=count/>
// desugars to


<counter
  value=count
  valueChange(newCount) {
    count = newCount;
  }
/>
counter value:=count
// desugars to


counter [
  value=count
  valueChange(newCount) {
    count = newCount;
  }
]

For Property Accessors, the change handler desugars to a member expression with a Change suffix.

<counter value:=input.count/>
// desugars to


<counter value=input.count valueChange=input.countChange/>
counter value:=input.count
// desugars to


counter value=input.count valueChange=input.countChange

Shorthand class and id

Emmet style class and id attribute shorthands are supported.

<div class="bar baz" id="foo"/>
// same as

<div id="foo" class="bar baz"/>
div.bar.baz#foo
// same as

div#foo.bar.baz

Tip

Interpolations are supported within a dynamic class/id.

<div class=`icon-${iconName}`/>
div class=`icon-${iconName}`

Shorthand value

It is common for a tag to use a single input property; therefore Marko allows a shorthand for passing an attribute named value. If the attribute name is omitted at the beginning of a tag, it will be passed as value.

<my-tag=1/>
// desugars to


<my-tag value=1/>
my-tag=1
// desugars to


my-tag value=1

The method shorthand can be combined with the value attribute to give us the value method shorthand.

<my-tag() {
  console.log("Hello JavaScript!");
}/>
// desugars to


<my-tag value() {
  console.log("Hello JavaScript!");
}/>
// Received by the child as { value() { ... } }

my-tag() {
  console.log("Hello JavaScript!");
}
// desugars to


my-tag value() {
  console.log("Hello JavaScript!");
}
// Received by the child as { value() { ... } }

Attribute Termination

Attributes can be terminated with a comma. This is useful in concise mode.

<my-tag a=1 b=2/>
my-tag a=1 b=2

Caution

Comma operators / sequence expressions must be wrapped in parentheses

<my-tag a=(console.log(foo), foo)/>
my-tag a=(console.log(foo), foo)

Tag Content

Markup within a tag is made available as the content property of its input.

<my-tag>Content</my-tag>
my-tag -- Content

The implementation of <my-tag> above can write out the content by passing its input.content to a dynamic tag:

<div>
  <${input.content}/>
</div>
div
  ${input.content}

Dynamic Text

Dynamic text content can be ${interpolated} in the tag content. This uses the same syntax as template literals in JavaScript.

<div>Hello ${input.name}</div>
div -- Hello ${input.name}

Note

The interpolated value is automatically escaped to avoid XSS.

Attribute Tags

Tags prefixed with an @ are not rendered, but instead passed alongside attributes in input. Attribute tags allow for passing named or repeated content as additional attributes.

<my-layout title="Welcome">
  <@header class="foo">
    <h1>Big things are coming!</h1>
  </@header>

  <p>Lorem ipsum...</p>
</my-layout>
my-layout title="Welcome"
  @header.foo
    h1 -- Big things are coming!

  p -- Lorem ipsum...

Here, @header is available to <my-layout> as input.header. The class attribute from @header is in input.header.class and its content is in input.header.content.

Note

Control flow tags (<if> and <for>) cannot contain attribute tags themselves, and instead are used for dynamically creating attribute tags.

The full input object provided to <my-tag> in this example would look like:

// a representation of `input` received by `my-layout.marko` (from the previous code snippet)
{
  title: "Welcome",
  header: {
    class: "foo",
    content: /* <h1>Big things are coming!</h1> */,
  },
  content: /* <p>Lorem ipsum...</p> */,
}

The implementation of my-layout.marko might look like

<!doctype html>
<html>
  <head>
    <title>${input.title}</title>
  </head>
  <body>
    <header class=(input.header.class)>
      <img src="./logo.svg" alt="...">
      // render the content of `@header`

      <${input.header.content}/>
    </header>

    <main>
      // render the main tag content

      <${input.content}/>
    </main>

    <footer>Copyright ♾️</footer>
  </body>
</html>
<!doctype html>
html
  head
    title -- ${input.title}
  body
    header class=(input.header.class)
      img src="./logo.svg" alt="..."
      // render the content of `@header`

      ${input.header.content}

    main
      // render the main tag content

      ${input.content}

    footer -- Copyright ♾️

Nested Attribute tags

Attribute tags may be nested in other attribute tags.

<my-tag>
  <@a value=1>
    <@b value=2/>
  </@a>
</my-tag>
my-tag
  @a value=1
    @b value=2

Would provide the following as input

{
  a: {
    value: 2,
    b: { value: 2 }
  }
}

Repeated Attribute Tags

When multiple attribute tags share a name, all instances may be consumed using the iterable protocol.

<my-menu>
  <@item value="foo">
    Foo Item
  </@item>

  <@item value="bar">
    Bar Item
  </@item>
</my-menu>
my-menu
  @item value="foo" -- Foo Item

  @item value="bar" -- Bar Item

This example uses two <@item> tags, but <my-menu> receives only a single item attribute.

{
  item: {
    value: "foo",
    content: /* Foo Item */,
    [Symbol.iterator]() {
      // Not the exact implementation, but essentially this is what the function contains
      yield* [
        { value: "foo", content: /* Foo Item */ },
        { value: "bar", content: /* Bar Item */ }
      ];
    }
  }
}

The other <@item> tags are reached through the iterator. The most common way to do so is with a for tag or one of JavaScript's syntaxes for iterables.

<for|item| of=input.item>
  Value: ${item.value}
  <${item.content}/>
</for>
for|item| of=input.item
  -- Value: ${item.value}
  ${item.content}

Tip

If you need repeated attribute tags as a list, it is a common pattern to spread into an array with a <const> tag

<const/items=[...(input.item || [])]/>

<div>${items.length}</div>
const/items=[...(input.item || [])]

div -- ${items.length}

Conditional Attribute Tags

Attribute tags are generally provided directly to their immediate parent. The exception to this is control flow tags (<if> and <for>), which are used to dynamically apply attribute tags.

<my-message>
  <if=welcome>
    <@title>Hello</@title>
  </if>
  <else>
    <@title>Good Bye</@title>
  </else>
</my-message>
my-message
  if=welcome
    @title -- Hello
  else
    @title -- Good Bye

In this case, the @title received by <my-message> depends on welcome.

<my-select>
  <@option>None</@option>

  <for|opt| of=["a", "b", "c"]>
    <@option>${opt}</@option>
  </for>
</my-select>
my-select
  @option -- None

  for|opt| of=["a", "b", "c"]
    @option -- ${opt}

Here, <my-select> unconditionally receives the first @option, and also all of the @option tags applied by the <for> loop.

Note

You can't mix attribute tags with default content while inside a control flow tag.

Tag Variables

Tag variables expose a value from a tag to be used within a template (from a custom tag, the variable is taken from its <return>). These variables are not quite like JavaScript variables, as they are used to power Marko's compiled reactivity.

Tag Variables use a / followed by a valid JavaScript identifier or destructure assignment pattern after the tag name.

<my-tag/foo/>
<my-other-tag/{ bar, baz }/>

<div>`my-tag` returned ${foo}</div>
<div>`my-other-tag` returned an object containing ${bar} and ${baz}</div>
my-tag/foo
my-other-tag/{ bar, baz }

div -- `my-tag` returned ${foo}
div -- `my-other-tag` returned an object containing ${bar} and ${baz}

Native tags have an implicitly returned tag variable that contains a reference to the element.

<div/myDiv/>

<script>
  myDiv().innerHTML = "Hello";
</script>
div/myDiv

script
  -- myDiv().innerHTML = "Hello";

In this case myDiv will be a variable which can be called to get the myDiv element in the browser.

Using the core <return> tag, any custom tag can return a value into it's parents scope as a tag variable.

Scope

Tag variables are automatically hoisted and can be accessed anywhere in the template except for in module statements. This means that it is possible to read tag variables from anywhere in the tree.

<form>
  <input/myInput>
</form>

<script>
  // still available even though it's nested in another tag.
  console.log(myInput());
</script>
form
  input/myInput

script
  --
  // still available even though it's nested in another tag.
  console.log(myInput());
  --

Tag Parameters

While rendering content, child may pass information back to its parent using tag parameters.

child.marko
<div>
  <${input.content} number=1337/>
</div>
div
  ${input.content} number=1337
parent.marko
<child|params|>
  Rendered with ${params.number} as the `number=` attribute.
</child>
child|params| -- Rendered with ${params.number} as the `number=` attribute.

This example results in the following HTML:

<div>Rendered with 1337 as the `number=` attribute</div>

The |parameters| are enclosed in pipes after a tag name, and act functionally like JavaScript function parameters within which the first parameter is an object containing all attributes passed from the child component.

Tip

Parameters include all features of the JavaScript function parameters syntax, so feel free to destructure.

<child|{ number }|>
  Rendered with ${number} as the `number=` attribute.
</child>
child|{ number }| -- Rendered with ${number} as the `number=` attribute.

Tag Arguments

Multiple tag parameters may be provided to the content by using the Tag Arguments syntax, which uses the JavaScript (...args) syntax after the tag name.

<${input.content}(1, 2, 3)/>
${input.content}(1, 2, 3)

This example passes three arguments back to its parent.

<my-tag|a, b, c|>
  Sum ${a + b + c}
</my-tag>
// spreads work also!

<my-tag|...all|>
  Sum ${all.reduce((a, b) => a + b, 0)}
</my-tag>
my-tag|a, b, c| -- Sum ${a + b + c}
// spreads work also!

my-tag|...all| -- Sum ${all.reduce((a, b) => a + b, 0)}

Warning

Tag content may use attributes or arguments, but not both at once.

<my-tag a=1 b=2 c=3/>
// identical to

<my-tag({ a: 1, b: 2, c: 3 })/>
my-tag a=1 b=2 c=3
// identical to

my-tag({ a: 1, b: 2, c: 3 })

Scope

Tag parameters are scoped to the tag content only. This means you cannot access the tag parameters outside the body of the tag.

Caution

Attribute tags cannot access the tag parameters of their parent since they are evaluated as attributes.

Comments

Both HTML and JavaScript comments are supported.

<div>
  <!-- html comments -->
  // JavaScript line comments

  /** JavaScript block comments */
</div>
div
  <!-- html comments -->
  // JavaScript line comments

  /** JavaScript block comments */

Note

Comments are ignored completely. To include a literal HTML comment in the output, use the <html-comment> core tag.

Dynamic Tags

In place of the tag name, an ${interpolation} may be used to dynamically output a native tag, custom tag, or tag content.

With a dynamic tag the closing tag should be </>, or if there is no content the tag may be self-closed.

Dynamic Native Tags

When the value of the dynamic tag name is a string,

// Dynamically output a native tag.

<${"h" + input.headingSize}>Hello!</>
// Dynamically output a native tag.

${"h" + input.headingSize} -- Hello!

Dynamic Custom Tags

// Dynamically output a custom tag.

import MyTagA from "<my-tag-a>";
import MyTagB from "<my-tag-b>";
<${Math.random() > 0.5 ? MyTagA : MyTagB}/>
// Dynamically output a custom tag.

import MyTagA from "<my-tag-a>";
import MyTagB from "<my-tag-b>";
${Math.random() > 0.5 ? MyTagA : MyTagB}

Caution

When rendering a custom tag, you must have a reference to it, the following is not equivalent to the above example. In this case, Marko will output a native HTML element (as if you called document.createElement("my-tag-a")).

<${Math.random() > 0.5 ? "my-tag-a" : "my-tag-b"}/>
${Math.random() > 0.5 ? "my-tag-a" : "my-tag-b"}

Note

If an object is provided with a content property, the content value will become the dynamic tag name. This is how the define tag works under the hood 🤯.

<define/Message>
  Hello World
</define>
<${Message}/>
define/Message -- Hello World
${Message}

Although in this case you should prefer a PascalCase <Message> tag instead.

Conditional Parent Tags

When a dynamic tag name is falsy it will output the tag's content only. This is useful for conditional parenting and fallback content.

// Only wrap the text with an anchor when we have an `input.href`.

<${input.href && "a"} href=input.href>
  Hello World
</>
// Only wrap the text with an anchor when we have an `input.href`.

${input.href && "a"} href=input.href -- Hello World

PascalCase Variables

Local variable names that start with an upper case letter (PascalCase) can also be used as tag names without the explicit dynamic tag syntax. This is useful for referencing an imported custom tag or with the <define> tag.

import MyTag from "./my-tag.marko";

<MyTag/>
import MyTag from "./my-tag.marko";

MyTag

This is equivalent to

import MyTag from "./my-tag.marko";

<${MyTag}/>
import MyTag from "./my-tag.marko";

${MyTag}

Contributors

Helpful? You can thank these awesome people! You can also edit this doc if you see any issues or want to improve it.