The First Compiled UI Framework That Renders at 120 FPS by Eliminating the DOM.
Native 120 FPS hardware rasterization β’ Zero DOM layout thrashing β’ Ghost DOM accessibility
CanvApps is not a canvas drawing library β it's a full compiled UI framework.
It compiles declarative .cvs Single-File Components into an HTML5 2D GPU-accelerated Canvas render tree at 120 FPS, backed by Ghost DOM for native text editing, mobile keyboards, and accessibility.
βββββββββββββββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββ
β π¨ 2D GPU CANVAS LAYER β π» GHOST DOM LAYER β
β (120 FPS Hardware Rasterization) β (Zero-Cost Semantic HTML Overlay) β
βββββββββββββββββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββ€
β β’ Pure Mathematical Flexbox β β’ Native iOS/Android Virtual Keyboardsβ
β β’ Fine-Grained Signals (No VDOM) β β’ Real Browser Text Selection & Copy β
β β’ Hardware-timed Kinetic & Motion FX β β’ Full A11y (VoiceOver / TalkBack) β
β β’ Pixel-Perfect DPR Retina Scale β β’ OS Context Menus & IME Compositions β
βββββββββββββββββββββββββββββββββββββββββ΄ββββββββββββββββββββββββββββββββββββββββ
- π 120 FPS β Direct Canvas 2D rendering with Retina/HiDPI scaling
- π« Zero DOM β Pure TypeScript Flexbox solver, no reflows
- π» Ghost DOM β Native keyboards, text selection, clipboard, screen readers
- β‘ Signals β
signal,computed,effect,batchβ no Virtual DOM diffing - π¨
.cvsSFC - components compiled at build time - ποΈ Motion & KineticFX β Declarative animations, hero morphs, particle bursts
- π¦ Multi-Target β SPA, PWA, or native mobile (Capacitor) from one config
npm create canvapps@latest my-app
cd my-app
npm install
npm run devAlso works with other package managers:
pnpm create canvapps my-app
yarn create canvapps my-app
bun create canvapps my-appnpm install @canvapps/core<!DOCTYPE html>
<html>
<head>
<style>html, body, #app { width: 100%; height: 100%; margin: 0; overflow: hidden; }</style>
</head>
<body>
<div id="app"></div>
<script src="https://cdn.jsdelivr.net/npm/@canvapps/core/dist/canvapps.umd.cjs"></script>
<script>
const { Engine, UIView, UIText, UIButton, signal, effect } = window.canvapps;
const count = signal(0);
const engine = new Engine({ container: '#app', autoResize: true, backgroundColor: '#0f172a' });
const root = new UIView({ width: '100%', height: '100%', alignItems: 'center', justifyContent: 'center', gap: 14 });
const text = new UIText('Clicks: 0', { fontSize: 22, color: '#ffffff' });
const btn = new UIButton('+ Click Me', { padding: [10, 20], backgroundColor: '#2563eb', labelColor: '#ffffff' });
btn.on('click', () => count.update(n => n + 1));
effect(() => text.setText(`Clicks: ${count.value}`));
root.addChild(text).addChild(btn);
engine.setRoot(root).start();
</script>
</body>
</html>Compiled at build time with zero runtime overhead. syntax:
<script lang="ts">
const count = signal(0);
const taskInput = signal('');
function onInput(e: any) {
taskInput.value = e?.target?.value ?? e?.value ?? '';
}
function addTask() {
const text = taskInput.value.trim();
if (!text) return;
tasks.update((list) => [{ id: Date.now(), title: text }, ...list]);
taskInput.value = '';
}
const tasks = signal([
{ id: 1, title: 'Eliminate DOM layout thrashing' },
{ id: 2, title: 'Test Ghost DOM on iOS & Android' },
]);
</script>
<view width="100%" height="100%" flexDirection="column" backgroundColor="#f8fafc" padding="[20, 32]" gap="16">
<!-- Dynamic Bindings -->
<text fontSize="18" :color={count.value > 5 ? '#dc2626' : '#0f172a'}>
Clicks: {{ count.value }}
</text>
<!-- Events -->
<button label="+1" @click="() => count.update(n => n + 1)" />
<!-- Inputs (Ghost DOM: native keyboards & clipboard) -->
<input placeholder="New task..." :value="taskInput.value" @input="onInput" @submit="addTask" />
<!-- List Iteration -->
@each tasks.value as item, index {
<view width="100%" padding="12" flexDirection="row" justifyContent="space-between">
<text fontSize="13" selectable="true">β’ {{ item.title }}</text>
</view>
}
<!-- Conditionals -->
@if (count.value > 10) {
<text color="#059669">π You passed 10 clicks!</text>
}
</view><view :gap={isMobile.value ? 6 : 12} :flexDirection={isMobile.value ? 'column' : 'row'}>
<button :backgroundColor={isActive.value ? '#2563eb' : '#64748b'} label="Toggle" />
</view>@if (isMobile.value) {
<text>π± Mobile Layout</text>
} else {
<text>π» Desktop Layout</text>
}@each tasks.value as item, index {
<view width="100%" padding="12">
<text selectable="true">β’ {{ item.title }}</text>
<button label="β" @click="() => removeTask(item.id)" />
</view>
}<script lang="ts">
import HomeView from './views/HomeView.cvs';
import SettingsView from './views/SettingsView.cvs';
const showSettings = signal(false);
</script>
<view width="100%" height="100%">
@if (showSettings.value) {
<SettingsView @close="() => showSettings.value = false" />
} else {
<HomeView />
}
</view><button label="Save" @click="handleSave" />
<button label="+1" @click="() => count.update(n => n + 1)" />
<input @input="onInput" @submit="onSubmit" />
<select :options="options" @change="onChange" />
<view @pointerenter="() => hovered.value = true" @pointerleave="() => hovered.value = false">
<text>Hover me</text>
</view>Supported events: @click, @dblclick, @pointerdown, @pointerup, @pointermove, @pointerenter, @pointerleave, @input, @change, @submit, @keydown, @keyup, @scroll, @wheel, @finish, @close.
<motion animation="cinematic-splash" :duration="1100" :hold="800" @finish="onSplashFinish">
<view width="100%" height="100%" backgroundColor="#090d16" alignItems="center" justifyContent="center">
<text fontSize="56" fontWeight="bold" color="#ffffff">CanvApps</text>
</view>
</motion>
<motion enter="elastic" exit="slide-left" :duration="450">
<GalleryView />
</motion>import { KineticFX } from '@canvapps/core';
KineticFX.flyToken({
from: { x: e.clientX, y: e.clientY },
to: '#counter-badge',
text: '+100 XP',
duration: 480,
onHit: () => score.value += 100,
});
KineticFX.burst({
x: e.clientX, y: e.clientY,
colors: ['#38bdf8', '#34d399', '#818cf8'],
count: 24,
});<modal :open="isOpen.value" :originRect="rect.value" animation="hero" :blur="true" @close="closeModal">
<view width="760" backgroundColor="#ffffff" borderRadius="20" padding="28">
<text fontSize="20" fontWeight="bold">Hero Expanded Dialog</text>
</view>
</modal>Assign matching :layoutId props to elements across views for fluid morph transitions:
<!-- Source -->
<a href="/docs" label="π Docs" :layoutId="'nav-docs'" />
<!-- Destination -->
<text :layoutId="'nav-docs'" fontSize="13" fontWeight="bold">π Docs</text>import { createStore, computed, persistentSignal } from '@canvapps/core';
export const sessionStore = createStore({
user: null,
isAuthenticated: false,
theme: 'light' as 'light' | 'dark',
}, { name: 'session', persist: true }); // Auto localStorage sync
export const isLoggedIn = computed(() => sessionStore.state.isAuthenticated);
export const authToken = persistentSignal('jwt_token', '');import { defineConfig } from '@canvapps/core';
export default defineConfig({
target: 'PWA', // 'SPA' | 'PWA' | 'CAPACITOR'
title: 'My App',
outDir: 'dist-app',
banner: true, // Open-source watermark in output
safeArea: true, // Auto notch/status bar insets
themeColor: { light: '#f8fafc', dark: '#090d16' },
pwa: {
name: 'My App',
shortName: 'App',
themeColor: '#2563eb',
display: 'standalone',
},
capacitor: {
appId: 'com.myapp.app',
appName: 'My App',
},
});<!-- Auto notch/status bar spacing -->
<view width="100%" safeArea="top" padding={[12, 20]}>
<text fontSize="18" fontWeight="bold">π¨ My App</text>
</view>import { useSafeArea, setThemeColor } from '@canvapps/core';
const { top, bottom } = useSafeArea();
// top.value => 47 on iPhone 15, 0 on desktop
setThemeColor('#101010'); // Updates status bar, titlebar, overscroll// vite.config.ts
import { defineConfig } from 'vite';
import canvappsPlugin from '@canvapps/core/vite';
export default defineConfig({
plugins: [canvappsPlugin()],
resolve: {
alias: [
{ find: /^@canvapps$/, replacement: '@canvapps/core' },
{ find: /^canvapps$/, replacement: '@canvapps/core' },
],
},
optimizeDeps: {
include: ['@canvapps/core'],
},
});# Create a new project
npm create canvapps@latest my-app
# Development server (Vite + HMR)
npm run dev
# Production build
npx canvapps build
# Format .cvs files
npx canvapps formatOfficial extension for VS Code, Cursor, Windsurf, and Antigravity IDE:
π Download canvapps-vscode-0.1.0.vsix
Features: Syntax highlighting, Go to Definition, autocompletion, snippets (cvs-component, cvs-view, cvs-button, etc.), and tag auto-closing for .cvs files.
# Install via CLI
code --install-extension canvapps-vscode-0.1.0.vsix| API | Description |
|---|---|
createApp(Component, options?) |
Create and configure an app instance |
app.mount('#container') |
Mount app to DOM element |
new Engine(options) |
Canvas renderer with 120 FPS loop |
UIView |
Flexbox layout container |
UIText |
Multiline text with optional selection |
UIButton |
Interactive button with hover/active states |
UIInput |
Text input with Ghost DOM keyboard sync |
UISelect |
Canvas dropdown selector |
UIModal |
Modal with hero morph & blur |
UIMotion |
Declarative scene transitions |
KineticFX |
Flying tokens & particle bursts |
signal(val) |
Reactive state primitive |
computed(fn) |
Derived reactive value |
effect(fn) |
Reactive side effect |
batch(fn) |
Batch multiple signal updates |
createStore(state, opts?) |
Global reactive store with persistence |
persistentSignal(key, val) |
Signal with localStorage sync |
createRouter(config) |
Client-side canvas router |
useSafeArea() |
Reactive device inset signals |
setThemeColor(color) |
Dynamic status bar/titlebar sync |
defineConfig(config) |
Typed canvapps.config.ts helper |
| DOM Frameworks | Flutter Web | CanvApps | |
|---|---|---|---|
| Rendering | Browser DOM | WebGL/Skia (Wasm) | Canvas 2D (GPU) |
| FPS | 30β60 | 45β60 | 60β120 |
| Layout | Browser reflows | Dart layout | Math Flexbox (TS) |
| Reactivity | VDOM diffing | Widget rebuild | Fine-grained signals |
| Keyboards | Native | Emulated | Native (Ghost DOM) |
| A11y | Native | Emulated | Native (Ghost DOM) |
| Bundle | MediumβHeavy | >2MB Wasm | <30KB |
MIT Β© Erickgiber