DEV Community

YQteam
YQteam

Posted on

Why we wrote our own 8.5 kB web component runtime instead of pulling in a framework

Hi, we're YQteam 👋 We just open-sourced yq-sanyi (v0.2.0, our initial release): define a component as a native HTML tag, then drop <yq-counter> into a plain page — zero dependencies, zero build.

The first question people ask us is: there are already so many component solutions — why build your own?

This post is not about the API. It's about the decision process behind the project.

A scenario we kept hitting

The need is simple: a reusable counter, a panel, a todo list — instead of copy-pasting the same HTML plus interaction logic into every page.

But in practice you usually have only two options:

  • Adopt a framework. For a "lightweight reuse" need, you pay for a whole runtime plus a build toolchain. For existing static pages, multi-page sites, or server-rendered template pages, that feels like moving house just to buy a shelf.

  • Hand-rolled copy-paste. Great the first time, painful from the second: boilerplate, styles bleeding into each other, event listeners nobody remembers to clean up.

We wanted a third way: as light as copy-paste, with the reuse experience of a framework.

The three paths we walked

Native Web Components: the right direction, but parts, not a product

customElements standardized "UI as an element" — that's genuinely great. But to actually use a component comfortably you still have to solve a long list of problems yourself:

  • How do attributes and internal state stay in sync?

  • How should the event system be designed?

  • Where does reactive state come from?

  • What about style isolation? If you reach for Shadow DOM, form-element piercing, CSS-variable theming, and injecting external styles all become design problems of their own;

  • Who cleans up listeners and side effects on unmount?

The standard doesn't build those wheels for you. Using it raw means boilerplate; filling the gaps means writing a framework anyway.

A framework runtime: the cost doesn't match the benefit

Our actual need was "a few reusable interactive widgets", not page-wide state management, routing, and an ecosystem. Bringing in the latter for the former is like opening a supermarket to buy a bottle of water.

This is especially true when the target is plain HTML / multi-page / static pages, or a page embedded inside someone else's system — a framework runtime plus build output can easily weigh more than the components themselves.

A minimal self-built loop: do just one thing — components

So we implemented it ourselves, shrinking the scope to the smallest coherent unit:

Let template, behavior and scoped style be one definition, one native tag, and one shared lifecycle.

The name "yq-sanyi" ("trinity") comes exactly from that idea.

The rules we set for ourselves

  • Web-standard APIs only. No JSX, no virtual DOM, no compiler, no framework runtime;

  • Single source of truth. Template, behavior and style live in one unit — easy to read, reuse and audit;

  • Mutate state, the view updates itself. Tags auto-mount, auto-update and auto-cleanup;

  • Style isolation without mandatory Shadow DOM. Selector-scoping by default, CSS-variable theming, opt-in Shadow DOM when you need strong encapsulation;

  • Failure isolation. A broken component renders an error placeholder plus a structured warning while the rest of the page keeps working;

  • Size is a budget, not a souvenir. The core is ~8.5 kB gzipped, and the repo ships dependency-graph and bundle-size gates plus benchmarks so it can't quietly bloat.

What it looks like now

yq.define('yq-counter', {
  template: '<button yq-on:click="inc">count {{ count }}</button>',
  style: 'button { font-size: 18px; padding: 8px 18px; }',
  script: function () {
    return {
      state: { count: 0 },
      inc: function (state) {
        state.count = state.count + 1
      }
    }
  }
})
Enter fullscreen mode Exit fullscreen mode

Define it once. After that, the usage site is a single line:

<yq-counter></yq-counter>
Enter fullscreen mode Exit fullscreen mode

For template syntax, state, lists and scoped styles, the README and the English tutorial cover everything — no need to repeat it here.

We're not telling anyone to abandon their framework

Different constraints call for different choices. If your project is deeply embedded in a framework ecosystem, needs SSR, or depends on a full CLI-driven pipeline — a framework is right for you, and we explicitly list those cases as "deliberately not supported" in our docs.

But if you're on the "I just want to reuse a piece of UI, lightly" side — no build, plain HTML pages, curious about how components actually work under the hood — come say hi at the repo:

v0.2.0 is our initial release and we will keep iterating: fixing known limitations first (event binding inside yq-for rows), evolving the template layer and devtools, and keeping the English/Chinese docs in sync. Stars, issues, and pull requests are all welcome — and if the project saves you time, you can support us on Afdian to help keep it free, open and zero-dependency.

Top comments (0)