Marko in March 2026

TL;DR
  • Bound attribute modifiers like value:Number:=quantity
  • "Did you mean" suggestions for unknown tags
  • Full Rolldown support and initial Vite 8 compatibility
  • Better support for apps embedded inside another app

March added a shorthand for the most common two-way binding chore, closed a nearly decade-old request for friendlier tag errors, and moved the Vite integration onto Rolldown. Around those were steady gains in output size, concise-mode whitespace handling, and Class-to-Tags interop.

Bound Attributes

Two-way bindings often need to normalize a value before storing it. A modifier placed on a bound attribute now does this inline: the identifier after the attribute name processes every value passed to the implicit change handler.

<let/quantity=1>
<input type="number" value:Number:=quantity>
<p>Subtotal: ${(quantity * 4.99).toFixed(2)}</p>
let/quantity=1
input type="number" value:Number:=quantity
p -- Subtotal: ${(quantity * 4.99).toFixed(2)}

Here value:Number:=quantity binds the input and coerces each change through Number before assigning it, replacing a longhand change handler that would otherwise be written out by hand. This is especially convenient when binding to a member expression, where the equivalent handler is verbose. The modifier must be an identifier, so more involved normalization can point at a local static function of the same name.

Editor support shipped alongside the runtime, so the shorthand formats and type-checks correctly (marko#3130, language-server#468). A related fix made bound attributes with an as cast parse correctly instead of erroring (language-server#465, marko#3110).

Clearer Errors

Continuing the recent push to catch mistakes earlier, the compiler now suggests a correction when it meets an unknown tag. A misspelled or unrecognized component name is met with the closest known tag as a suggestion rather than a bare failure, closing a request that had been open for nearly ten years (marko#3114). Invalid attribute names and malformed bind refinement shorthands now produce messages as well, so more authoring mistakes surface at compile time (marko#3133).

Rolldown

The Vite integration gained full Rolldown support, replacing the previous esbuild-based plugin with a dedicated Rolldown plugin (vite#251). Vite 8 is now accepted as a peer dependency, with the remaining esbuild interop to be removed as the plugin settles onto Rolldown (vite#246). Dev-mode dependency scanning was reworked to pre-scan entry templates with htmljs-parser, resolve their tags through the taglib, and register the results in a single pass rather than several (vite#253).

Embedded Apps

Embedding a Marko app inside another page, such as a widget mounted into an existing document, works more smoothly. When a template is detected not to render html, head, or body, Marko now generates a unique $global.renderId automatically, and it cleans up scopes when the embedded DOM is removed (marko#3124).

Improvements

Beyond the headlines, improvements trimmed server output and expanded type coverage.

Performance

Server output got smaller. Duplicate section serialization guards are now emitted once instead of repeatedly (marko#3141). The list reconciliation code was rewritten with its logic inlined for a smaller bundle and additional fast paths for the common case where no items move between renders (marko#3112).

Type Definitions

Native attribute typings expanded, adding webkitdirectory on <input>, media on <meta>, and refined <button> attributes (marko#3121, marko#3137, marko#3138).

Marko Run

Marko Run's file-based routing gained an interactive REPL at marko.run/repl that visualizes how a set of route files maps to matched routes (marko.run#1).

Fixes

Correctness work concentrated on concise-mode whitespace and the Class API interop layer.

Concise Syntax

Several concise-mode whitespace regressions were corrected. Newlines between nodes inside a concise HTML block are preserved again rather than collapsed, and trailing whitespace in those blocks is handled correctly (htmljs-parser#216, htmljs-parser#218). The Tags API <style> translation was updated for the parser's revised text nodes, and the compiler no longer inserts a stray space between adjacent text and placeholders in a tag body (marko#3108, marko#3129).

Interop

Mixing the Class API and Tags API grew more reliable when legacy renderer APIs are involved, with dynamic tags routing correctly through the compatibility layer (marko#3145). A production-mode ordering bug in return-tag analysis was fixed, so intersecting state resolves in the correct order (marko#3135). The @marko/express middleware was updated for Marko 6 (express#12).

In Brief

  • Rendering no longer throws an uninitialized error when input is absent (marko#3125).
  • The compiler avoids loading Babel configuration when Babel is not installed (marko#3118, marko#3119).

Full details for every change are in the release notes of each package on GitHub.

Further Reading


Contributors

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