No bundler. No framework. Just web standards, TypeScript, and a CLI that compiles, documents, and type-checks your components.
scaffolds a component + demo in seconds
# 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
/** @csspart button */ class MyButton extends HTMLElement { static observedAttributes = ['variant']; /* … */ }
$ 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
Vanilla custom elements, shadow DOM, modern CSS. There's no runtime to ship and nothing new to learn — it's the platform.
A single component yields a manifest, .d.ts, VS Code / JetBrains IntelliSense, and docs — all in sync, all generated.
banira test smoke-mounts every element; banira diff reads the manifest and suggests a semver bump.
new in 0.5
The manifest is the start. banira now lints, server-renders, themes and ships your components — still vanilla, still no bundler.
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.
banira lint audits each element for attribute↔property reflection, overridable :host styles, and documented events, attributes, parts & slots — advisory by default, --strict for CI.
banira stories writes CSF with an argTypes controls panel from the manifest — string-literal unions become select options, events become actions.
--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.
tokens-css compiles W3C DTCG tokens to CSS, and theme scaffolds a light/dark contract plus a <theme-toggle>.
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
<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.
/** * @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" }]
}
A button custom element with variant + size attributes.
| Attr | Type | Values |
|---|---|---|
| variant | string | primary · secondary · ghost |
| size | string | sm · md · lg |
::part(button) · Events: click