vanilla web components · no runtime

A toolchain for vanilla web components.

No bundler. No framework. Just web standards, TypeScript, and a CLI that compiles, documents, and type-checks your components.

$ npx banira init my-button src
Read the docs →

scaffolds a component + demo in seconds

~/my-button — zsh
# scaffold a component + its demo
$ npx banira init my-button src

# watch · compile · serve · live-reload
$ npx banira dev src/my-button.ts -o demo/dist -r demo
  ✓ compiled my-button.ts → demo/dist/my-button.js (12ms)
  ➜ serving demo/ on http://localhost:8080 _

the payoff

One source file. Many artifacts.

src/my-button.ts
/** @csspart button */
class MyButton extends HTMLElement {
  static observedAttributes
    = ['variant'];
  /* … */
}
banira ▸
{ }
custom-elements.json
the manifest, single source of truth
TS
elements.d.ts
types + JSX intrinsic elements
{}
editor IntelliSense
VS Code & JetBrains data
</>
my-button.html
a doc page, PicoCSS by default
SB
my-button.stories.js
Storybook CSF + controls
DSD
prerendered HTML
Declarative Shadow DOM, SSR-ready
generate everything
$ banira manifest src/*.ts        -o custom-elements.json
$ banira types    src/*.ts        -o dist/elements.d.ts
$ banira doc      src/my-button.ts -o docs/my-button.html

why banira

Three reasons, no hand-waving.

Standards, not abstractions

Vanilla custom elements, shadow DOM, modern CSS. There's no runtime to ship and nothing new to learn — it's the platform.

One source, many artifacts

A single component yields a manifest, .d.ts, VS Code / JetBrains IntelliSense, and docs — all in sync, all generated.

Built for CI

banira test smoke-mounts every element; banira diff reads the manifest and suggests a semver bump.

new in 0.5

From source to ship — the whole pipeline.

The manifest is the start. banira now lints, server-renders, themes and ships your components — still vanilla, still no bundler.

Server rendering & hydration

prerender emits Declarative Shadow DOM (FOUC-free, critical CSS inlined); createPrerenderer / the Eleventy plugin render on the server and hydrateShadow adopts the markup on the client.

A Gold Standard linter

banira lint audits each element for attribute↔property reflection, overridable :host styles, and documented events, attributes, parts & slots — advisory by default, --strict for CI.

Storybook, generated

banira stories writes CSF with an argTypes controls panel from the manifest — string-literal unions become select options, events become actions.

No-bundler imports

--import-map pins bare imports to esm.sh so vanilla components use import … from 'lit' in the browser with no build — squarely banira's ethos.

Design tokens & theming

tokens-css compiles W3C DTCG tokens to CSS, and theme scaffolds a light/dark contract plus a <theme-toggle>.

A sharper manifest

String-literal union attributes become enums in the .d.ts, editor data and Storybook; --validate checks the official CEM JSON Schema, and --link-package wires it into package.json.

live proof

A real <my-button>, defined in vanilla JS.

Drive the element below — it's a genuine custom element with shadow DOM, running on this page. The same kind of source produces its manifest and its doc page; flip the tabs.

Get started
variant
size
state click fired ×0
/**
 * @element my-button
 * @attr {string} variant - primary | secondary | ghost
 * @attr {string} size    - sm | md | lg
 * @csspart button
 * @fires click
 */
export class MyButton extends HTMLElement {
  static observedAttributes = ['variant', 'size'];

  connectedCallback() {
    this.attachShadow({ mode: 'open' });
    this.render();
  }
  /* render reads variant + size attrs */
}
customElements.define('my-button', MyButton);
{
  "tagName": "my-button",
  "customElement": true,
  "attributes": [
    { "name": "variant",
      "type": { "text": "string" } },
    { "name": "size",
      "type": { "text": "string" } }
  ],
  "cssParts": [{ "name": "button" }],
  "events":   [{ "name": "click" }]
}
▸ docs/my-button.html · PicoCSS

<my-button>

A button custom element with variant + size attributes.

AttrTypeValues
variantstringprimary · secondary · ghost
sizestringsm · md · lg
CSS parts: ::part(button)  ·  Events: click
currently: <my-button variant="primary" size="md">
phased rollout · additive, ship incrementally
phase 0 · done
TypeDoc API on Pages
phase 1 · done
Landing + Getting Started
phase 2 · done
CLI pages + guides
phase 3 · done
Live demo on landing
phase 4 · maybe
In-browser playground