Courvux templates are plain HTML with directives and expression bindings. All expressions are full JavaScript (requires no strict CSP).
Use inside text nodes for reactive values:
<!-- Text interpolation <!-- Text interpolation -->
<p>{{ count }}</p>
<p>{{ price * qty }}</p>
<p>{{ active ? 'On' : 'Off' }}</p>
<p>{{ name.toUpperCase() }}</p>
Prefix any attribute with : to evaluate it as a JavaScript expression:
<!-- Property binding <!-- Property binding -->
<input :disabled="count > 10" />
<img :src="avatarUrl" :alt="user.name" />
<!-- Class binding (object | array | string) <!-- Class binding (object | array | string) -->
<div :class="{ active: isOn, 'text-muted': !isOn }"></div>
<div :class="['base', isOn ? 'on' : 'off']"></div>
<!-- Style binding <!-- Style binding -->
<span :style="{ color: textColor, fontSize: size + 'px' }"></span>
<span :style="'color:red; font-weight:bold'"></span>
Use @event or cv:on:event. Access the raw event via $event.
<!-- Method reference <!-- Method reference -->
<button @click="increment">+1</button>
<!-- Inline expression <!-- Inline expression -->
<button @click="count++">+</button>
<button @click="count = 0">Reset</button>
<!-- $event — raw DOM event <!-- $event — raw DOM event -->
<input @input="search = $event.target.value" />
<!-- Modifiers <!-- Modifiers -->
<form @submit.prevent="onSubmit">...</form>
<button @click.stop="doThing">...</button>
<button @click.once="runOnce">...</button>
<!-- Key modifiers <!-- Key modifiers -->
<input @keydown.enter="submit" />
<input @keydown.esc="cancel" />
<!-- cv:on: prefix (alternative to @) <!-- cv:on: prefix (alternative to @) -->
<button cv:on:click="increment">+1</button>
Add :key for keyed reconciliation — Courvux reuses existing DOM nodes for matching keys.
<!-- Array <!-- Array -->
<li cv-for="item in items">{{ item }}</li>
<li cv-for="(item, index) in items">{{ index }}: {{ item }}</li>
<!-- Object <!-- Object -->
<li cv-for="(val, key) in person">{{ key }}: {{ val }}</li>
<!-- Keyed — recommended for dynamic lists <!-- Keyed — recommended for dynamic lists -->
<li cv-for="user in users" :key="user.id">
{{ user.name }}
</li>
Nodes are inserted and removed from the DOM.
<!-- Elements are added/removed from the DOM <!-- Elements are added/removed from the DOM -->
<p cv-if="count > 10">High</p>
<p cv-else-if="count > 0">Low</p>
<p cv-else>Zero</p>
Toggles display: none. Node stays in the DOM.
<!-- Toggles display:none — stays in DOM <!-- Toggles display:none — stays in DOM -->
<div cv-show="isVisible">Panel content</div>
<!-- With Alpine-style transition <!-- With Alpine-style transition -->
<div cv-show="open" cv-transition>Fade in/out</div>
<div cv-show="open" cv-transition.scale>Scale + fade</div>
<!-- Text input <!-- Text input -->
<input type="text" cv-model="name" />
<!-- Checkbox → boolean <!-- Checkbox → boolean -->
<input type="checkbox" cv-model="active" />
<!-- Select <!-- Select -->
<select cv-model="country">
<option value="us">United States</option>
<option value="mx">Mexico</option>
</select>
<!-- Modifiers <!-- Modifiers -->
<input cv-model.lazy="query" /> <!-- update on blur <!-- update on blur -->
<input cv-model.trim="username" /> <!-- strip whitespace <!-- strip whitespace -->
<input cv-model.number="price" /> <!-- coerce to number <!-- coerce to number -->
<input cv-model.debounce="search" /> <!-- 300ms debounce <!-- 300ms debounce -->
<input cv-model.debounce.500="search" /> <!-- custom delay <!-- custom delay -->
Sets innerHTML. Sanitized by default — strips <script>, on*= handlers, and javascript: URLs so user-submitted content is safe to render. Add .raw to opt out when the markup is something you authored (Markdown rendered server-side, hand-curated copy).
<!-- Sanitized by default — strips <script>, on*= handlers, javascript: URLs.
Safe for user-submitted content. , on*= handlers, javascript: URLs.
Safe for user-submitted content. -->
<div cv-html="userContent"></div>
<!-- Opt out of sanitization with .raw — only for content YOU authored
(Markdown rendered server-side, hand-curated HTML, etc.) <!-- Opt out of sanitization with .raw — only for content YOU authored
(Markdown rendered server-side, hand-curated HTML, etc.) -->
<div cv-html.raw="myTrustedContent"></div>
cv-html.sanitize still works (it's now a no-op). To restore the old raw behavior on a binding you control, switch cv-html → cv-html.raw.
Self-contained reactive scope without component registration. Lighter than components — no lifecycle, no slots, no emits.
<!-- Inline reactive scope — no component registration needed <!-- Inline reactive scope — no component registration needed -->
<div cv-data="{ count: 0 }">
<button @click="count--">−</button>
<span>{{ count }}</span>
<button @click="count++">+</button>
</div>
<!-- With methods <!-- With methods -->
<div cv-data="{ open: false, toggle() { this.open = !this.open } }">
<button @click="toggle()">{{ open ? 'Close' : 'Open' }}</button>
<p cv-show="open">Content</p>
</div>
<!-- Nested scopes — child inherits parent keys <!-- Nested scopes — child inherits parent keys -->
<div cv-data="{ user: 'Alice' }">
<div cv-data="{ role: 'admin' }">
{{ user }} — {{ role }}
</div>
</div>
<!-- cv-once — render once, skip future updates <!-- cv-once — render once, skip future updates -->
<strong cv-once>{{ initialValue }}</strong>
<!-- cv-ref — store element reference in $refs <!-- cv-ref — store element reference in $refs -->
<input cv-ref="myInput" />
<!-- cv-teleport — move to another DOM node <!-- cv-teleport — move to another DOM node -->
<div cv-show="modal" cv-teleport="body">...</div>
<!-- cv-cloak — hide until mounted <!-- cv-cloak — hide until mounted -->
<div id="app" cv-cloak></div>