DEV Community

Umeko
Umeko

Posted on

I built a Web Components language that compiles to nothing

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.

Chasket website | GitHub


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 closed mode 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>
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 eventsemit 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
Enter fullscreen mode Exit fullscreen mode

You'll have a working project with a sample component in seconds.

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)