I've always been drawn to the space between abstraction and reality — taking an idea and watching it become something concrete. That fascination led me to build Chasket, a template-first language for Web Components that compiles .csk files into native HTMLElement classes with zero runtime.
No virtual DOM. No framework bundle. Just the platform.
Why I built this
I write backend code for my day job (Spring Boot, PostgreSQL), but I've always been curious about the frontend. When I started exploring Web Components, I liked the idea — framework-agnostic, encapsulated, native — but the developer experience was rough.
Writing a simple button with vanilla Custom Elements means class extends HTMLElement, attachShadow(), observedAttributes, attributeChangedCallback... just too much boilerplate for what should be simple.
I also wanted:
-
Shadow DOM
closedmode for real encapsulation — most tools don't make this easy - Type checking at compile time — catch typos before they hit the browser
- Scoped CSS by default — no BEM, no naming tricks, just isolation
- No framework lock-in — the output should work anywhere, forever
So I built a compiler that takes a simple .csk file and outputs a plain Custom Element class.
What a .csk file looks like
Here's a counter component:
<meta>
name: "x-counter"
shadow: open
</meta>
<script>
state count: number = 0
fn increment() { count += 1 }
fn decrement() { count -= 1 }
</script>
<template>
<div class="counter">
<button @click="decrement">-</button>
<span>{{ count }}</span>
<button @click="increment">+</button>
</div>
</template>
<style>
.counter { display: flex; gap: 12px; }
button { width: 36px; border-radius: 8px; }
span { font-size: 1.5rem; }
</style>
Four blocks: meta, script, template, style. Declare state and the DOM updates automatically. No hooks, no store imports, no boilerplate.
This compiles to something like:
class XCounter extends HTMLElement {
#count = 0;
connectedCallback() { /* ... */ }
#increment() { this.#count++; }
}
customElements.define('x-counter', XCounter);
That's it. No runtime library attached. The output is a native Custom Element that works in any framework — or no framework at all.
Built-in features
Chasket handles the things you'd normally need separate tools for:
Reactivity — Declare state and the DOM stays in sync. No signals library needed.
Expressive templates — <#if> / <#for> for control flow, :bind for two-way binding, @click|prevent for event modifiers.
Compile-time type checking — Catches undefined variables, type mismatches, and invalid attribute bindings before you run anything.
// Compiler output:
error: Undefined 'coutn'
error: Expected number, got string
XSS protection — All interpolation is automatically escaped. Raw HTML requires explicit @html opt-in. URL validation is built in.
Scoped styles — Shadow DOM isolates CSS by default. No class naming conventions needed.
Typed events — emit declares typed custom events with bubble/compose control.
The ecosystem
Chasket isn't just a compiler. I've built out the packages you need to make real applications:
| Package | What it does |
|---|---|
@chasket/chasket |
Compiler, dev server, CLI |
@chasket/chasket-router |
SPA routing (history/hash mode, nested routes, guards) |
@chasket/chasket-store |
State management with actions, getters, undo/redo |
@chasket/chasket-ssr |
Server-side rendering with streaming and hydration |
@chasket/chasket-ui |
Pre-built accessible components (buttons, inputs, dialogs) |
@chasket/chasket-lsp |
Language Server for VS Code, Neovim, Sublime |
@chasket/vite-plugin-chasket |
Vite integration with HMR and source maps |
The VS Code extension is now available on the VS Code Marketplace — syntax highlighting, diagnostics, go-to-definition, and formatting out of the box.
How it compares
| Chasket | React | Vue | Svelte | Lit | |
|---|---|---|---|---|---|
| Runtime size | 0 KB | ~40 KB | ~33 KB | ~2 KB | ~5 KB |
| Web Components | Native | - | Partial | Partial | Native |
| Single-file components | Yes | - | Yes | Yes | - |
| Built-in type checking | Yes | - | - | - | - |
| Scoped CSS | Shadow DOM | CSS Modules | Scoped | Scoped | Shadow DOM |
| SSR | Yes | Yes | Yes | Yes | Yes |
The closest comparison is probably Lit, which also targets Web Components natively. The key difference: Lit ships a ~5 KB runtime for its reactive base class. Chasket resolves everything at compile time, so the output has no dependency at all.
What I learned building this
I'm about 1.5 years into my career as an engineer, and this project taught me a lot.
Dogfooding matters. I built chasket.dev using Chasket itself. The compiler had 356 passing tests, but real usage uncovered bugs that no test had caught — CSS scoping edge cases, component ID collisions, documentation examples that didn't match the actual compiler behavior.
"It works" and "people can use it" are completely different things. Making the compiler functional was one challenge. Making the error messages clear, the npx init experience smooth, and the documentation navigable — that took just as much effort.
Package design should come first. I started monolithic and later split into @chasket/* scoped packages. That refactoring was painful. If you're building a library with multiple concerns, plan your package boundaries early.
Try it
npx @chasket/chasket init my-app
cd my-app
npm run dev
You'll have a working project with a sample component in seconds.
- Website: chasket.dev
- GitHub: UltraEgoist/chasket
- npm: @chasket/chasket
This is an early-stage project (currently v0.3.0) and I'd genuinely love feedback. If you try it and something breaks or feels wrong, please open an issue. Every bug report makes this better.
I'm a backend engineer from Japan exploring the frontend world. I built Chasket because I believe abstraction should lead to something real — and that the best abstraction is one that disappears completely.
Top comments (0)