Essential syntax, patterns, and common pitfalls for working with Marko.js v6 components.
- Component Syntax
- Body Content
- Attributes
- Conditionals
- Common Pitfalls
- Best Practices
- Troubleshooting
<!-- ✅ CORRECT: kebab-case -->
<my-component></my-component>
<user-card></user-card>
<alert-box></alert-box>
<!-- ❌ WRONG: PascalCase or camelCase -->
<MyComponent></MyComponent>
<myButton></myButton><!-- ✅ CORRECT: space before slash -->
<icon name="check" />
<alert-box kind="tip" />
<!-- ❌ WRONG: no space -->
<icon name="check"/>
<alert-box kind="tip"/><!-- ✅ CORRECT: unquoted for dynamic values -->
<a href=input.link>Link</a>
<div class=["active", input.isCurrent && "current"]></div>
<!-- ✅ CORRECT: quoted for static values -->
<a href="/about">About</a>
<div class="container">Content</div>
<!-- ❌ WRONG: quoted dynamic values -->
<a href="${input.link}">Link</a>
<div class="${input.class}">Content</div>To render the content between opening and closing tags:
<!-- ✅ CORRECT: Use input.content -->
<div class="my-component">
<${input.content}/>
</div>
<!-- Usage in markdown -->
<my-component>
This is the body content
</my-component><!-- ❌ WRONG: Renders as <input class=text> HTML element -->
<div><input.text/></div>
<!-- ❌ WRONG: Renders as <input> HTML element -->
<div><input/></div>
<!-- ❌ WRONG: Renders as literal <body> tag -->
<div><body/></div>
<!-- ❌ WRONG: Expects input.renderBody which doesn't exist -->
<div><${input.renderBody}/></div>For components with multiple content areas:
<!-- Component: card.marko -->
<div class="card">
<if=input.header>
<div class="card-header">
<input.header/>
</div>
</if>
<if=input.body>
<div class="card-body">
<input.body/>
</div>
</if>
<if=input.footer>
<div class="card-footer">
<input.footer/>
</div>
</if>
</div>
<!-- Usage -->
<card>
<card-header><h3>Title</h3></card-header>
<card-body>Content here</card-body>
<card-footer>Footer info</card-footer>
</card><!-- ✅ CORRECT -->
<div class=["alert", input.kind && "alert-" + input.kind, !input.kind && "alert-default"]>
Content
</div>
<!-- Results: -->
<!-- kind="note" → class="alert alert-note" -->
<!-- kind="tip" → class="alert alert-tip" -->
<!-- kind undefined → class="alert alert-default" --><!-- ✅ CORRECT: || operator -->
<a href=input.link || "#">Link</a>
<!-- ✅ CORRECT: inline ternary -->
<div class=input.type ? "type-" + input.type : "type-default">
Content
</div>AVOID these attribute names as they're reserved in HTML:
<!-- ❌ WRONG: type is reserved (used by input, button, etc.) -->
<alert-box type="note">Content</alert-box>
<!-- ✅ CORRECT: Use kind or variant instead -->
<alert-box kind="note">Content</alert-box>
<alert-box variant="note">Content</alert-box>Other reserved names to avoid:
id- Useuid,itemId, etc.class- UseclassName(but class is OK if using array syntax)style- Usecssorstylesname- Usekind,variant, etc.value- UsedefaultValue,content, etc.for- UsehtmlFor(label context)
<!-- ✅ CORRECT: no parentheses around expression -->
<if=input.isVisible>
<div>This is visible</div>
</if>
<!-- ✅ CORRECT: with else -->
<if=input.type === "primary">
<button-primary>Click</button-primary>
<else/>
<button-secondary>Click</button-secondary>
</if>
<!-- ❌ WRONG: parentheses around expression -->
<if(input.isVisible)>
<div>This is visible</div>
</if>
<!-- ❌ WRONG: if() syntax -->
<if(input.isVisible)>
<div>This is visible</div>
</if><!-- ✅ CORRECT: pipe syntax -->
<for|item| of=input.items>
<div>${item.name}</div>
</for>
<!-- ✅ CORRECT: with index -->
<for|item, index| of=input.items>
<div>${index + 1}. ${item.name}</div>
</for>
<!-- ✅ CORRECT: range -->
<for|i| from=0 to=10>
<div>Item ${i}</div>
</for>CRITICAL: Marko v6 does NOT support >, <, >=, <= operators in <if> tag attributes. These are output as literal text.
<!-- ❌ WRONG: Outputs "0>" as text -->
<if=input.items.length > 0>
Content
</if>
<!-- ❌ WRONG: Outputs "<" as text -->
<if=input.index < 10>
Content
</if>
<!-- ✅ CORRECT: Compute in let variable first -->
<let/hasItems= (input.items && input.items.length)>
<if=hasItems>
Content
</if>
<!-- ✅ CORRECT: For comparisons, use valid operators -->
<let/isValidIndex= (input.index <= 10)>
<if=isValidIndex>
Content
</if>CRITICAL: When a <let> variable evaluates to 0, passing it to <if> outputs "0>" as text.
<!-- ❌ WRONG: Returns 0 for empty array, outputs "0>" -->
<let/hasChildren= (input.item.children && input.item.children.length)>
<if=hasChildren>
Content
</if>
<!-- ✅ CORRECT: Use !! for proper boolean conversion -->
<let/hasChildren= !!(input.item.children && input.item.children.length)>
<if=hasChildren>
Content
</if>CRITICAL: Self-closing custom components may output /> as text in HTML.
<!-- ❌ WRONG: May output "/>" as text -->
<theme-toc-item item=item/>
<my-component data=input.data/>
<!-- ✅ CORRECT: Use explicit closing tags -->
<theme-toc-item item=item></theme-toc-item>
<my-component data=input.data></my-component>
<!-- ✅ OK: Self-closing HTML elements work fine -->
<img src=input.imagePath/>
<input value=input.text/>
<br/><!-- ❌ WRONG: Not supported in Marko v6 -->
<div class="alert alert-${input.type}">
Content
</div>
<!-- ✅ CORRECT: Use array syntax -->
<div class=["alert", "alert-" + input.type]>
Content
</div>
<!-- ✅ CORRECT: Use ternary -->
<div class=input.type ? "alert alert-" + input.type : "alert">
Content
</div><!-- ❌ WRONG: Quoted dynamic values -->
<div class="${input.className}">Content</div>
<!-- ✅ CORRECT: Unquoted for dynamic -->
<div class=input.className>Content</div>
<!-- ✅ CORRECT: Array for classes -->
<div class=["base-class", input.className]>Content</div><!-- ✅ CORRECT: Direct text -->
<div>Hello ${input.name}</div>
<!-- ❌ WRONG: Using <input.text/> for body content -->
<div><input.text/></div>
<!-- ✅ CORRECT: For body content between tags -->
<div><${input.content}/></div>Problem: Components in tags/ directory not rendering
Solution:
- Ensure tags are copied to build output
- Use kebab-case filenames:
alert-box.marko✅,AlertBox.marko❌ - Rebuild after adding new components
<!-- ✅ GOOD: Clear, documented component -->
<!--
Alert Box Component
Types: note, tip, warning, danger, info, caution
Example: <alert-box kind="warning">Important!</alert-box>
-->
<div class=["alert", input.kind && "alert-" + input.kind, !input.kind && "alert-note"]>
<${input.content}/>
</div><!-- ✅ GOOD: Descriptive, non-conflicting names -->
<my-component
kind="primary" <!-- NOT type -->
variant="large" <!-- NOT size -->
isDisabled=true <!-- NOT disabled -->
onItemClick=handleClick <!-- NOT onClick -->
>
Content
</my-component><!-- ✅ GOOD: Provide sensible defaults -->
<div class=["base-class", input.modifier && "base-" + input.modifier]>
<${input.content}/>
</div>
<a href=input.link || "#">
<input.text/>
</a><!-- ✅ GOOD: Simple conditionals -->
<if=input.showHeader>
<div class="header">
<input.header/>
</div>
</if>
<!-- ✅ GOOD: Multiple conditions -->
<div class=[
"alert",
input.kind === "error" && "alert-error",
input.kind === "warning" && "alert-warning",
!input.kind && "alert-default"
]>
<${input.content}/>
</div>Symptom:
<div class="alert alert-${input.type || 'note'}"><input class=text></div>Causes:
- Template literal used in attribute → Use array syntax
- Tags directory not copied to dist → Ensure build copies tags
- Component not discovered → Rebuild project
Symptom:
<div class="alert"></div>
<!-- Content is missing -->Causes:
- Using
<input.text/>→ Use<${input.content}/> - Using
<input/>→ Use<${input.content}/> - Using
<body/>→ Use<${input.content}/> - Attribute name mismatch → Check component expects
input.content
Symptom:
<!-- All components have default values -->
<div class="alert alert-note"></div>
<div class="alert alert-note"></div>Causes:
- Using reserved attribute name (e.g.,
type) → Usekind,variant, etc. - Attribute value not being passed → Check compiled output for function call
- Component needs rebuild → Delete
dist/and rebuild
Common errors:
Tag does not support arguments. <if(expression)>
Solution: Change to <if=expression> (no parentheses)
Missing semicolon
Solution: Remove script blocks or fix syntax (Marko v6 has limited script support)
<!--
Component Name
Description of what it does
Attributes:
- kind: Component variant (required)
- size: Size variant (optional, default: "medium")
Example: <my-component kind="primary">Content</my-component>
-->
<div class=[
"my-component",
input.kind && "my-component-" + input.kind,
input.size && "my-component-" + input.size
]>
<${input.content}/>
</div>- ✅ Component name is kebab-case
- ✅ File is in
tags/directory - ✅ Using
<${input.content}/>for body content - ✅ Not using reserved attribute names
- ✅ Using array syntax for dynamic classes
- ✅ Using
<if=expression>not<if(expression)> - ✅ NO comparison operators (
>,<) in<if>attributes - ✅ Using
!!for boolean conversion in<let>variables - ✅ Using explicit closing tags for custom components
- ✅ Rebuilt after changes
- ✅ Tags copied to
dist/
- Marko Documentation: https://markojs.com/docs/
- @marko/run Docs: https://github.com/marko-js/run
- MarkoPress Component Guide:
MARKO_COMPONENTS_GUIDE.md
Last Updated: 2025-01-17 (updated 2025-01-25)
Version: Marko.js v6, @marko/run v0.9.4