Register globally via app.directive() or per-component via directives.
// Full definition
app.directive('focus', {
onMount(el, binding) { el.focus(); },
onUpdate(el, binding) { /* reactive update */ },
onDestroy(el, binding) { /* cleanup */ }
});
// Shorthand — mount only
app.directive('highlight', (el, binding) => {
el.style.background = binding.value ?? 'yellow';
});
// Per-component directives
{
directives: {
tooltip: { onMount(el, b) { ... }, onDestroy(el) { ... } }
}
}
<!-- Plain <!-- Plain -->
<input cv-focus />
<!-- With value (reactive) <!-- With value (reactive) -->
<p cv-highlight="activeColor">Text</p>
<!-- With argument and modifiers <!-- With argument and modifiers -->
<div cv-pin:top.once="offset"></div>
| Binding property | Description |
|---|---|
value | Evaluated expression (reactive in onUpdate) |
arg | Argument after : — cv-pin:top → 'top' |
modifiers | Flags — cv-pin.once → { once: true } |
Animate elements entering and leaving. Works on any element with cv-show.
<!-- Bare cv-transition — fade (built-in) <!-- Bare cv-transition — fade (built-in) -->
<div cv-show="open" cv-transition>Panel</div>
<!-- Fade + scale <!-- Fade + scale -->
<div cv-show="open" cv-transition.scale>Panel</div>
<!-- Scale origin + duration <!-- Scale origin + duration -->
<div cv-show="open" cv-transition.scale.90.duration.400>Panel</div>
<!-- Class-based (Alpine-compatible) — any CSS framework <!-- Class-based (Alpine-compatible) — any CSS framework -->
<div
cv-show="open"
cv-transition:enter="transition ease-out duration-300"
cv-transition:enter-start="opacity-0 scale-95"
cv-transition:enter-end="opacity-100 scale-100"
cv-transition:leave="transition ease-in duration-200"
cv-transition:leave-start="opacity-100 scale-100"
cv-transition:leave-end="opacity-0 scale-95"
>Panel</div>
<!-- <cv-transition> component (built-in named animations) component (built-in named animations) -->
<cv-transition name="fade" :show="open">
<div>Animated content</div>
</cv-transition>
<!-- Fire when element enters viewport <!-- Fire when element enters viewport -->
<div cv-intersect="loadMore()">...</div>
<!-- Separate enter / leave handlers <!-- Separate enter / leave handlers -->
<div
cv-intersect:enter="onEnter()"
cv-intersect:leave="onLeave()"
>...</div>
<!-- Modifiers <!-- Modifiers -->
<div cv-intersect.once="trackImpression()">...</div>
<div cv-intersect.half="handler()">...</div> <!-- 50% visible <!-- 50% visible -->
<div cv-intersect.threshold-75="handler()">...</div>
<div cv-intersect.margin-200="prefetch()">...</div>
A plugin is an object with an install(app) method. Install before mounting. Duplicate installs are silently ignored.
const analyticsPlugin = {
install(app) {
if (app.router) {
const prev = app.router.afterEach;
app.router.afterEach = (to, from) => {
prev?.(to, from);
analytics.track(to.path);
};
}
}
};
createApp(config)
.use(analyticsPlugin)
.mount('#app');
Register a $name property available in every component template.
// Register a global $name property available in every component
createApp(config)
.magic('fmt', () => ({
currency: (val) => new Intl.NumberFormat('en-US', {
style: 'currency', currency: 'USD'
}).format(val),
date: (val) => new Date(val).toLocaleDateString(),
}))
.magic('http', () => axios)
.mount('#app');
// In any template:
// <p>{{ $fmt.currency(price) }}</p>
// <button @click="$http.post('/api/save', data)">Save</button>
Initialize cv-data elements automatically — no createApp() required. Ideal for server-rendered HTML.
import { autoInit } from 'courvux';
// Scans [cv-data] elements on DOMContentLoaded
autoInit({
components: { dropdown: DropdownDef },
directives: { tooltip: myDirective },
globalProperties: { appName: 'My Site' }
});