---
title: "Building Vel: A Token-Efficient, Compile-to-Native UI Language"
date: "2026-06-04"
description: "Notes from designing Vel — a declarative DSL with a reactive substrate, Skia rendering, and first-class C/C++ FFI. Flutter-class capabilities, far less source."
slug: "building-vel"
tldr: "Vel is a three-tier UI stack: a small indentation-sensitive DSL (.vel) compiling to C++, a Skia-backed framework (libvel), and a 43-widget shadcn-style registry. It targets 4–6× source-token compression vs. JSX while keeping first-class C/C++ interop and ~0 idle CPU."
tags: ["uilang", "dsl", "cpp", "skia"]
cover: "/images/blog/building-vel.svg"
coverAlt: "Diagram of Vel's three-tier architecture: language compiles to framework which composes a widget registry consumed by application code"
author: "Sai Chandan Kadarla"
devto_id: 3843016
---

I've been building **Vel** — a new declarative UI language plus the runtime and component registry to back it. The goal is unapologetically ambitious: write production desktop apps with the expressive density of Tailwind classes, the reactivity model of SolidJS, and the rendering quality of Flutter, while letting you call into any existing C/C++ backend without a binding layer.

This post walks through the architecture I landed on, why each tier exists, and what the language actually buys you in practice.

## The three-tier rule

Most UI frameworks blur three concerns: the language, the runtime substrate, and the opinionated widget set. Vel splits them on purpose, with a strict one-way dependency:

```
language  →  framework  →  registry  →  application
(velc)       (libvel)       (vel::ui::*)    (.vel + main.cpp)
```

- **Language**: a small, indentation-sensitive DSL (`.vel`) and a single-pass compiler (`velc`). It parses to an AST, type-checks against a primitive registry, and emits idiomatic modern C++ that consumes the framework.
- **Framework (`libvel`)**: the substrate. Widget pipeline (measure / place / paint / tick), Skia-backed Painter, an event dispatcher, the reactive primitives (`Signal<T>`, `Computed<T>`, `AsyncSignal<T>`), and infra (router, storage, net shim, IO, time, validate, notify, JSON, typed context). No application policy — no themes, no shortcuts, no business rules.
- **Registry (`vel::ui::*`)**: 43 baseline widgets in the shadcn idiom — Btn, Card, Tabs, Accordion, Dialog, Sheet, Table, TreeView, Stepper, Image, and so on. Built by composing framework primitives. Third parties can ship their own registries as plain `.vel` files; the language has cross-file imports as a first-class feature.
- **Application**: your `.vel` files and a thin `main.cpp` shell. Themes, keyboard shortcuts, and routing decisions live here.

Code in a higher tier never reaches into a lower one. The framework never imports an icon set. The runtime never hardcodes a key combo. This isn't religion; it's the only way the language stays composable as third-party registries appear.

## Why a DSL — and why compile to C++?

Two reasons.

First, **source-token density**. The same UI in JSX vs. Vel:

```jsx
<View style={{padding: 24, gap: 12, backgroundColor: theme.bg0, borderRadius: 12}}>
  <Text style={{color: theme.fg, fontWeight: '700'}}>Hello</Text>
</View>
```

```vel
V p=lg g=md bg=bg0 r=md
  T "Hello" font=bold
```

That's roughly a 4–6× compression on real components without losing the declarative shape. For LLMs writing UI, that's the difference between fitting a feature in context and not. Vel was designed from day one with agent-authored code as a primary user.

Second, **no VM at runtime**. Vel compiles directly to typed C++ that calls into a static `libvel`. There's no interpreter loop, no JIT, no garbage collector. Layout, paint, and event dispatch are all monomorphic virtual calls on stable widget instances. On idle, the app sits in `glfwWaitEventsTimeout` and uses ~0 CPU — a global frame-dirty flag short-circuits the entire pipeline until something changes.

## The reactive substrate

The keystone primitive is `Signal<T>` — a value plus a listener list. Everything reactive flows from it.

In the DSL, `@state` declarations become typed `Signal<T>` members on the generated component:

```vel
#Counter
  @count = 0
  @doubled = $count * 2          // detected as derived → Computed<int>
  @users: std::vector<User> = await fetchUsers()  // → AsyncSignal<...>

  V g=md
    T "Count: {$count}, doubled: {$doubled}" font=bold
    Btn "++" -> click => $count = $count + 1
    if $users.loading
      Spinner
    else
      for u in $users.value
        T "{u.name}"
```

What velc emits, semantically:

- `Signal<int> count_{0}` — plain reactive cell.
- `Computed<int> doubled_{ [this]{ return count_.get() * 2; } }` — the codegen walks the init expression's AST, finds `$count` as a dependency, and binds the computed to it. Any `count_.set(...)` propagates: count → computed → component → `needsRebuild_ = true`.
- `AsyncSignal<std::vector<User>> users_` — kicked off with `std::async(std::launch::async, ...)`, polled each tick. Exposes `.loading` / `.ready` / `.value` / `.error` as method calls in the DSL.
- The component constructor `.listen()`s every signal it owns. The next `tick()` calls `rebuild()`, re-measures, and re-places before the frame paints. Tree rebuilds are coarse-grained — like React's `setState`, not Solid's fine-grained reactivity — but the granularity is set per `#Component`, so it composes cleanly.

Effects (`~ $a $b => body`) compile to per-dep listeners — declarative side effects without polluting event handlers.

## First-class C/C++ interop

The single most important constraint I set: **you must be able to drop any existing C++ codebase into Vel with one line.** No bindings, no schemas, no IDL. The language gives you two primitives:

```vel
use "myorg/db.h"        // raw C++ include — verbatim
use "ui/card.vel"        // cross-file vel import — pulls in components
```

Once a header is in scope, qualified names work in every expression. The lexer learned `::` so `db::fetchUsers()`, `std::vector<myorg::User>`, and `await myapp::loadAsync()` all parse and codegen straight through to the underlying calls. The framework knows nothing about your backend — velc just emits the include and the call site. Your existing C++ links into the binary like any other translation unit.

This is the reason Vel exists at all. Every other "new UI language" I've tried makes interop a second-class citizen. Here, the interop is part of the type system from day one — state can be of any C++ type, expressions can call any user namespace, and the DSL's type annotations support templates.

## Architecture of the rendering pipeline

The render path is Skia all the way down, but Vel adds a few layout patterns worth noting:

- **Constraints model**: identical to Flutter (min/max W/H), one measure pass returns intrinsic size, one place pass writes final rects. A small but important fix in Vel — when the cross axis is unbounded (e.g. inside a scrollable), `align=Stretch` degrades to `Start` so children don't get infinite size cascades.
- **Frame-level damage tracking**: an atomic `frameDirty` flag, raised by any `Widget::markDirty()` call, controls whether the next frame runs at all. Animating widgets re-arm the flag from their `tick()`; static pages don't.
- **Portal layer for overlays**: tooltips, popovers, dialogs, sheets, menus, and toasts go through an `OverlayHost` that owns a per-frame portal queue. Widgets nested inside scrolled containers register their panel rect plus the current scroll offset, so the panel paints in screen space regardless of where in the tree it was declared. This solved the classic "popover gets clipped by a Card with `clipContent`" problem cleanly.
- **DPR awareness**: the GL context is created at framebuffer resolution, the widget tree lays out in logical points, and the canvas matrix is scaled on each paint. Resize is handled by re-creating the Skia surface against the new framebuffer.

## The registry as a primitive, not a library

The 43-widget baseline ships in `vel/ui/`, but the registry mechanic isn't reserved for the framework. Any `.vel` file is a registry artifact. Example:

```vel
// widgets/stat.vel
#Stat label:str value:str hint:str=""
  Card elev=1
    V g=xs
      T $label fg=fgMuted font=uiSm
      T $value font=bold/display
      if $hint != ""
        T $hint fg=fgFaint font=uiSm
```

```vel
// showcase.vel
use "widgets/stat.vel"

#Showcase
  H g=md
    Stat label="MAU" value="42,103" hint="last 30 days"
    Stat label="Latency" value="84ms" hint="p99"
    Stat label="Errors" value="0.02%" hint="↓ 18%"
```

velc resolves the import, parses the imported file to extract the component's param signature, and at the call site emits `std::make_unique<vel::gen::Stat>("MAU", "42,103", "last 30 days")` — positional constructor args, type-checked statically by the C++ compiler downstream. Third parties can ship registries as bare `.vel` files via git, vendoring, or eventually a package manifest.

## What's working today

- Compiler with lexer, parser (forward-progress-guarded), type checker, codegen, and `--watch` mode.
- Reactive substrate: `Signal<T>`, `Computed<T>`, `AsyncSignal<T>`, `PersistentSignal<T>` (auto-syncs to disk), effects, typed `ctx<T>(key)` Provider substrate.
- Framework primitives: `Router` (path matching, history), `storage` (JSON-backed KV), `net` (pluggable HTTP shim), `io` (file ops + watcher), `time` (debounce / throttle / interval / timeout), `validate` (form rules), `notify` (toast bus), `json` (wraps nlohmann), per-widget cursor states, theme switching with full tree rebuild.
- Registry: 43 widgets including Table (virtualized, sortable, multi-select), TreeView, Stepper, Image (Skia-decoded with LRU cache), and the full shadcn-style overlay set.
- DX: agent-readable `widgets.json` regenerated on every build; cross-file imports; `--watch` for hot iteration.

## What's next

The big open items are hot reload (file watch is in; `dlopen` swap of the generated `.so` is not), a real `vel.json` package manifest, per-widget damage rectangles (right now damage is frame-level), and a few more registry primitives — Video, SVG, devtools overlay, a syntax-highlighted code editor.

If the three-tier story holds — and so far it does — Vel ends up being the smallest source surface area you can use to ship a production native desktop app, with the deepest backend interop story of any framework I'm aware of.

I'll keep posting as the registry grows.
