Write Motoko. Ship interactive, SEO-friendly ICP apps. No frontend JavaScript.
MotoView is a Motoko-native, server-driven UI framework for Internet Computer (ICP) canisters. You write .mview files — template markup and Motoko code together in a single file — and MotoView compiles them to Motoko and deploys them to the IC with dfx. If you've enjoyed Blazor, Blade, or LiveView, this will feel familiar.
There is no application JavaScript, no React, no agent-js glue, and no duplicated validation. Your canister renders the page, your handlers run on-chain, and the browser stays in sync automatically.
Rendering is a query, events are updates, and the browser synchronizes through versioned UI batches.
Building a frontend for an ICP canister usually means standing up a second project: a JavaScript bundler, a React app, agent-js to call the canister, and a second copy of your validation logic that has to stay in lockstep with the backend. MotoView removes that whole layer.
- One language, one source of truth. Markup and Motoko live side by side in
.mviewfiles. Your validation runs in your handlers — there is no second copy to keep in sync. - No Node, no npm, no JS build tooling. This is a core design goal, not an accident. The compiler is a Rust binary, the runtime is a Motoko library, and the only browser code is a small WebAssembly client. You never run a JavaScript bundler.
- Server-driven UI. The canister renders HTML. The first load is fully server-rendered for SEO and fast paint; after that, the browser synchronizes through versioned UI batches.
- Events are real Motoko functions. A
@click="save"dispatches to a typed Motoko handler that runs on-chain. Handler arguments are evaluated server-side and baked into the markup — you just write the handler. - Security in the platform. Secure forms mint a signed HMAC-SHA256 token binding the path, handler, caller principal, a nonce, an expiry, and the field-schema hash. The server re-derives the MAC and rejects mismatches, expired tokens, and replays. SHA-256 and HMAC are implemented in Motoko and verified against standard test vectors.
- Adaptive, considerate polling. The client polls fast right after an interaction and backs off when the tab is idle or hidden, so live updates feel instant without hammering the canister.
MotoView builds on the standard IC toolchain plus the Rust WebAssembly target.
# dfx — the DFINITY SDK
sh -ci "$(curl -fsSL https://internetcomputer.org/install.sh)"
# Rust + the wasm32 target (for the browser client)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
rustup target add wasm32-unknown-unknown
# wasm-opt (from binaryen) for optimizing the client wasm
brew install binaryen # or your platform's package managerThe motoview compiler is a Rust crate. Build and install it from this repository with cargo.
cargo install --path compilermotoview new my-app
cd my-appmotoview dev builds your .mview files to Motoko, deploys to a local replica with dfx, and watches for changes.
motoview devThen open the local URL printed by dfx in your browser. Click a button — the event hits the canister, your Motoko handler runs, a new UI batch comes back, and the DOM updates. No page reload, no JavaScript you had to write.
This is src/Pages/Counter.mview from the verified counter example — markup and Motoko, together:
@page "/"
@layout MainLayout
@title "Counter"
@description "A live counter built with MotoView — rendering is a query, the +/- clicks are updates."
<section class="mv-container">
<h1>Counter</h1>
<p class="counter-value">Current value: <strong>@count</strong></p>
<div class="counter-actions">
<button class="mv-btn mv-btn-primary" @click="increment(1)">+1</button>
<button class="mv-btn mv-btn-primary" @click="increment(5)">+5</button>
<button class="mv-btn mv-btn-secondary" @click="decrement">-1</button>
<button class="mv-btn mv-btn-ghost" @click="reset">Reset</button>
</div>
</section>
@code {
var count : Nat = 0;
func increment(by : Nat) : async () {
count += by;
};
func decrement() : async () {
if (count > 0) { count -= 1 };
};
func reset() : async () {
count := 0;
};
}The @click="increment(5)" handler argument is evaluated server-side at render time and baked into the markup; the WASM client forwards the handler id and its args, and the server dispatches to the typed increment function.
The runtime, the WASM client, and the dfx pipeline are verified end to end in a real browser on a local replica. Six examples ship under examples/, each compiled from .mview and exercised live:
examples/counter— the canonical demo. Clicking updates state viaevent -> update -> batch -> DOM swap; adaptive polling picks up external changes; state persists across calls.examples/contact— a secure, server-validated form. Invalid submits show persistent field errors; valid submits succeed. HMAC-SHA256 tokens + replay protection (verified against standard SHA-256/HMAC test vectors).examples/crm— a CRM sales-pipeline Kanban with drag-and-drop between stages (plus ◀ ▶ move buttons), create/delete, live pipeline totals, toast notifications, and column pulse animations. The deal logic lives insrc/Services/Crm.mo(real Motoko); the page holds the state. No frontend JavaScript — the drag-and-drop is the WASM client talking to on-chain handlers.examples/products— a full CRUD catalog: a keyed product table, a secure server-validated create form, an edit form on a typed route (/edit/{id:Nat}), and delete — backed by an upgrade-stableCatalogservice.examples/blog— an SEO-first blog: per-post<title>/<meta description>/<link canonical>, server-rendered, with each post served as a@cacheablecertified query and a 404 fallback for unknown slugs.examples/svg-network— an interactive SVG network graph: click a node to highlight it and its neighbors (a side panel lists them), reset to clear — all server-driven, no frontend JavaScript.
cd examples/crm && dfx start --background && dfx deploy
# open the printed http://<canister-id>.localhost:4955/ — drag a card between columnsBeyond the examples, apps/bzzz is a production-scale dogfood: Bzzz, a Discord × X × Discourse-style-forum × WhatsApp super-app — 9 stateful services and 15 pages, with Internet Identity login, upgrade-stable persistence, and @cacheable certified pages. It's how the Production features below were built and verified, including on the IC mainnet boundary via the playground.
.mviewfiles: template markup plus Motoko in one file.- Directives:
@page(including typed routes like@page "/orders/{id:Nat}"),@layout,@title,@description,@canonical,@meta,@authorize,@cacheable,@section/@yield,@slot,@code,@style,@theme,@if/else,@for,@switch, inline output (@count,@user.name,@(expr)),@effect(Focus / ScrollTo / Toast), and@animate. - Events:
@click,@submit,@input,@change— dispatched to typed Motoko functions, with handler args evaluated server-side. - Secure forms with
bind="@model.field", signed HMAC-SHA256 tokens, and replay/expiry rejection. - Handler-side validation (
validate model { ... }) with<ValidationSummary />and per-field errors. - Built-in semantic components —
Button,Card,Alert,Badge,InputText/InputEmail/InputNumber/TextArea,ValidationSummary,Table,PageHeader,Grid— plus your own components insrc/Components/*.mview. - Drag-and-drop: mark cards with
data-mv-dragand drop zones withdata-mv-drop/data-mv-dropval; a drop dispatches a typed Motoko handler — fully server-driven (see the CRM example). - Effects from handlers: call
toast("Saved"),animate("#panel", "pulse"),focusOn("#email")orscrollTo(...)from@codeand they ride back in the render batch. - Services & models: put real Motoko in
src/Services/*.moandsrc/Models/*.mo; the compiler imports them into the generated actor so page@codecan call them. - The
motoview/1protocol: server-rendered first load, batch-based sync withchanged/unchanged/redirect/validation-errorstatuses, and content-hashedbatchIds so unchanged batches skip re-rendering. - Adaptive polling: hot (~350ms after an interaction), warm (~2.5s while visible), cold (~15s when idle), hidden (~45s), and exponential backoff when offline — with event responses returning the new batch immediately.
These shipped while building the Bzzz reference app (see below) and are verified end-to-end — locally and, where it matters, against the IC mainnet boundary on the playground.
- Upgrade-stable persistence. A service that exposes
mvStableSave() : Blob/mvStableLoad(Blob)(a Candid round-trip) gets an auto-generatedstable varpluspreupgrade/postupgradehooks, so its state survivesdfx deploy --mode upgrade. Opt-in, per service, idempotent. Seedocs/persistence.md. - Internet Identity login — no npm, no agent-js. A hand-written browser IC agent (Ed25519 / CBOR / request-id / Candid, pure JS) makes one authenticated call; the runtime mints an httpOnly session cookie and resolves
ctx.callerfrom it. Every canister serves it at/mv-auth.js(auto-injected) — drop in a<button data-mv-signin>and you have login. Includes per-principal session revocation andii-alternative-origins. - Certified query rendering. Static framework assets and pages marked
@cacheableare served as fast certified queries (HTTP response-certification v2) instead of upgrading to an update call. Parameterized cacheable routes are covered by a single wildcard certificate (/u/{handle}→/u/<*>). Implemented from scratch in Motoko (runtime/src/CertV2.mo) and verified accepted by the mainnet boundary. - App components.
src/Components/*.mviewcompile to real typed render functions —params with defaults, the default@childrenslot, and named@slot/@section. - Hardened auth & forms. raw_rand-derived HMAC secret (never derived from public input), login-CSRF nonce binding, a pending-login DoS cap, origin-pinned
postMessage, andhttpOnly+Securecookies. motoview check. Builds and type-checks the generated actor, mapping anymocerrors back to the originating.mview(not the generatedmain.mo).- Regression test suite.
make testruns the parser/codegen tests that pin the framework's behavior.
compiler/ Rust crate — the motoview binary (parses .mview, generates Motoko)
runtime/ Motoko library (the "motoview" mops package) — serves HTTP from the canister
client/ Rust → WebAssembly browser client + tiny hand-written JS glue (no bundler)
examples/ counter, contact (secure form), crm (Kanban), products (CRUD), blog (SEO), svg-network
apps/ bzzz (reference super-app) and site (this docs + marketing site, itself a MotoView canister)
docs/ documentation (markdown — the source the site renders)
skills/ AI agent skills for working with MotoView
A typical generated project uses: src/Pages/*.mview, src/Layouts/*.mview, src/Components/*.mview, src/Services/*.mo, src/Models/*.mo, plus motoview.json, dfx.json, and mops.toml. The compiler emits a Motoko actor.
MotoView ships an agent skill under skills/motoview/ so AI coding tools can scaffold pages, wire up events, and write secure forms that match the framework's real APIs. See docs/ai-tools.md for how to point your assistant at it.
Full documentation lives in docs/ — start with docs/introduction.md and docs/quickstart.md, then dig into pages and routing, events, forms, validation, security, components, and the protocol.
Internet Identity login, certified query rendering, and upgrade-stable persistence have shipped (see Production features). Still planned and not yet implemented:
- Keyed-region / granular DOM patches (instead of root swaps).
- Role stores for
@authorize role="…". - vetKeys-encrypted state.
- A schema-migration story for evolving persisted service types.
- A visual designer.
Realtime is the adaptive-polling render/event protocol — there's no separate push transport, and a canister can't open a WebSocket without an external gateway. The polling protocol is the communication layer.
MotoView is released under the MIT License.