Appearance
Getting started
Requirements
Volt targets current engines and carries no legacy support. You need Node 22+ and pnpm 10+.
Install
bash
::: warning Under active development
Volt is pre-alpha. Nothing here is stable: APIs may change shape between
versions without a deprecation path, and there is no support commitment.
Pin an exact version if you build anything on it.
:::
pnpm add @voltdev/core@alpha
pnpm add -D @voltdev/vite-plugin@alpha viteConfigure Vite
ts
// vite.config.ts
import { defineConfig } from 'vite';
import { volt } from '@voltdev/vite-plugin';
export default defineConfig({
plugins: [volt()],
build: { target: 'esnext' },
});The plugin is required, not a convenience. It does two things nothing else in the toolchain currently does:
- Lowers TC39 standard decorators. They are stage 3 and implemented by no JavaScript engine. Vite 8 transforms with oxc, which parses decorators but emits them untouched, so
@Componentwould reach the browser as a syntax error. - Compiles templates at build time, so the compiler never ships to the browser and no template is parsed at runtime.
No build step?
Import @voltdev/core/jit once at startup to compile templates in the browser instead. Convenient for prototypes and playgrounds — but it ships the compiler, so prefer the plugin for anything real.
TypeScript configuration
json
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ESNext", "DOM", "DOM.Iterable", "ESNext.Decorators"],
"strict": true,
"useDefineForClassFields": true,
"experimentalDecorators": false
}
}experimentalDecorators must be off. Volt uses standard decorators; the legacy transform is a different feature with different semantics and is not supported.
Your first component
ts
// src/counter.ts
import { Component, Signal } from '@voltdev/core';
@Component({
selector: 'v-counter',
templateUrl: './counter.html',
styleUrl: './counter.scss',
})
export class Counter {
count = new Signal.State(0);
increment() {
this.count.set(this.count.get() + 1);
}
decrement() {
this.count.set(this.count.get() - 1);
}
}html
<!-- src/counter.html -->
<div class="counter">
<button :click="decrement()">−</button>
<output>{ count.get() }</output>
<button :click="increment()">+</button>
</div>scss
// src/counter.scss
.counter {
display: flex;
align-items: center;
gap: 0.5rem;
}Stylesheets are Sass. styleUrl takes a .scss file and nothing else — a plain .css file is rejected with a message saying so. Both paths resolve relative to the .ts file, are compiled at build time, and are watched, so editing the markup or the styles hot-reloads on its own. Partials pulled in with @use are watched too.
Mount it
ts
// src/main.ts
import { mount } from '@voltdev/core';
import { Counter } from './counter.js';
mount(Counter, '#app');html
<!-- index.html -->
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>mount returns a handle:
ts
const app = mount(Counter, '#app');
app.instance; // the component instance
app.unmount(); // disposes every effect it created, then clears the hostUsing one component from another
A template may only reference components listed in its imports. There is no global registry — this keeps resolution explicit and lets bundlers see the dependency.
ts
@Component({
selector: 'v-app',
imports: [Counter],
templateUrl: './app.html',
})
export class App {}Next
- Components — props, callbacks, lifecycle
- Reactivity — signals, computeds, effects
- Templates — the
:syntax in full