!!! WARNING: This is a work in progress. Please use at your own risk.
Rationale and background in a blog post
banira.js is an open-source toolchain designed for the development of web components using vanilla JavaScript. It simplifies the process by eliminating the need for bundlers and frameworks, focusing instead on modern CSS and web standards.
banira ships as a single package: a library you import, and a banira
command-line tool for compiling components and generating documentation.
# Library + CLI
npm install banira
# Or use the CLI without installing
npx banira --helpimport { Compiler, ResultAnalyzer, DocGen, ManifestGenerator } from 'banira';
// Compile a component to browser-ready ES modules
// (uses Compiler.DEFAULT_COMPILER_OPTIONS unless you pass your own)
const compiler = new Compiler(['src/my-button.ts']);
const analyzer = new ResultAnalyzer(compiler.emit());
if (analyzer.diag().hasErrors) throw new Error('compilation failed');
// Generate an HTML documentation page (summary, @demo, and a full API reference)
const page = await new DocGen('my-button').generate('src/my-button.ts');
// Generate a Custom Elements Manifest, then derive artifacts from it
const manifest = new ManifestGenerator(['src/my-button.ts']).generate();
import { manifestToMarkdown, toTypeDefinitions } from 'banira';
const apiDocs = manifestToMarkdown(manifest); // Markdown API tables
const dts = toTypeDefinitions(manifest); // typed HTMLElementTagNameMap| Export | Description |
|---|---|
| Compiler | Uses tsc to compile TypeScript files to JavaScript |
| TestHelper | Mount web components in JSDOM (including ones that import sibling modules) with deterministic readiness — the mount context exposes shadow-piercing query/queryAll — or, optionally, a real browser via Playwright (mountInBrowser) |
| DocGen | Generates an HTML documentation page (summary, @demo, and a full API reference) for a component |
| ManifestGenerator | Produces a Custom Elements Manifest (custom-elements.json) from vanilla components |
| manifestToMarkdown | Renders a manifest as Markdown API documentation |
| toVsCodeHtmlData / toVsCodeCssData / toWebTypes | Generate editor IntelliSense data (VS Code custom-data, JetBrains web-types) from a manifest |
| toTypeDefinitions | Generates a .d.ts typing the custom elements from a manifest (string-literal union attributes become union types) |
| toStories / manifestToStories | Generate Storybook CSF (*.stories.js) with argTypes from a manifest |
| validateManifest | Structurally validates a manifest against the CEM 2.1.0 shape |
| validateManifestSchema | Validates a manifest against the official CEM JSON Schema (requires the optional ajv dependency) |
| linkManifestField | Point a package.json's customElements field at a generated manifest |
| checkReflection / checkSlots | Smoke-test add-ons: attribute↔property reflection round-trip and @slot contract assertions |
| lintManifest | Audit components against the Gold Standard Checklist + documentation coverage (banira lint) |
| diffManifests | Diffs two manifests and suggests a semver release type |
| createPrerenderer / declarativeShadowDom | SSR primitive: register components once, then renderToString(tag, { attributes, children }) to Declarative Shadow DOM |
| createEleventyPlugin | Eleventy plugin that prerenders matching component tags to DSD at build time |
| hydrateShadow | Client hydration helper: adopt a prerendered DSD shadow root (no flash) or render + adopt styles |
| parseDesignTokens / designTokensToCss | Parse a W3C Design Tokens (DTCG) document and emit :root CSS custom properties (aliases resolved) |
| tokensToCssProperties / enrichManifestCssProperties | Map imported tokens to manifest cssProperties, or backfill missing defaults/descriptions on matching component tokens |
| scaffoldTheme | Generate a light/dark theme contract (theme.css), a <theme-toggle> component, and a demo page |
| buildImportMap / generateImportMap | Scan components' bare imports and build an import map pinning each package to esm.sh (compile --import-map, serve --import-map) |
banira <command> [options]Compile one or more TypeScript files with banira's compiler defaults.
| Option | Description |
|---|---|
-p, --project <path> |
Path to a tsconfig.json whose options override the defaults |
-o, --output <path> |
Directory to write the emitted JavaScript to |
--import-map [path] |
Also emit an import map for the components' bare imports (see below) |
--optimize-css |
Run inlined CSS through lightningcss (lower nesting, minify) before it's adopted |
banira compile src/my-button.ts -o dist
# also write dist/import-map.json pinning bare imports to esm.sh
banira compile src/my-button.ts -o dist --import-map--optimize-css needs the optional lightningcss dependency (npm i -D lightningcss),
loaded lazily — a clear error is shown if it's requested but not installed.
Compilation emits a .js.map source map next to each .js, with the original
TypeScript embedded, so breakpoints in devtools land on the .ts even when the
source isn't served alongside the output. Because the map embeds your source,
pass --no-source-map for production builds where you don't want to ship it.
With --import-map, banira walks the components' local module graph for bare
imports (e.g. import { html } from 'lit'), pins each package to
esm.sh at the version declared in package.json, and writes
an import map
(import-map.json by default; pass a path to override). This lets vanilla
components use bare specifiers with no bundler — add the file's contents as a
<script type="importmap"> before your module scripts. Unresolved bare imports
are expected in this mode (they resolve from the CDN at runtime), so they don't
fail the build.
Generate an HTML documentation page for a component. Combines the TSDoc summary
and @demo blocks with a full API reference — attributes, properties, methods,
events, slots, CSS parts and CSS custom properties — derived from the manifest.
Writes to stdout unless -o is given.
| Option | Description |
|---|---|
-o, --output <path> |
Write the page to a file instead of stdout |
--script-src <path> |
Component module src used in the page (default ./dist/<tag>.js) |
--stylesheet <value> |
A URL, a local .css file to inline, or none (default: PicoCSS CDN) |
By default the page links PicoCSS from a CDN, so it needs network access to be
styled. For a fully offline / self-contained page, pass a local .css file to
--stylesheet — it is inlined into the page.
banira doc src/my-button.ts -o docs/my-button.html
# self-contained / offline page: inline a local stylesheet, point at a custom build
banira doc src/my-button.ts --stylesheet ./theme.css --script-src ./my-button.js
# no stylesheet at all
banira doc src/my-button.ts --stylesheet noneGenerate a Custom Elements Manifest
(custom-elements.json) — the ecosystem-standard descriptor that powers IDE
autocomplete, Storybook controls and template type-checking. Writes to stdout
unless -o is given.
banira manifest src/*.ts -o custom-elements.jsonAttributes are read from observedAttributes, properties/methods from public
class members, events from new CustomEvent(...), and slots / CSS parts / CSS
custom properties from class jsdoc tags (@slot, @csspart, @cssprop, @fires).
A class-level @role tag records the element's default ARIA role (typically set
via ElementInternals.role). Members marked @deprecated carry the note
through; @internal / @ignore members are omitted.
| Option | Description |
|---|---|
-o, --output <path> |
Write the output to a file instead of stdout |
--md |
Emit Markdown API documentation instead of JSON |
--validate |
Validate the generated manifest and print a report (exit 1 on errors) |
--link-package |
Point the nearest package.json's customElements field at the written manifest |
--validate runs banira's fast structural checks. Install the optional
ajv dependency (npm i -D ajv) to additionally validate
the manifest against the official CEM JSON Schema for guaranteed spec-conformance,
with precise path-level errors.
--link-package sets "customElements": "<path>" in the nearest package.json
(the convention IDEs
and Storybook use to auto-discover a package's manifest), preserving the file's
indentation. It's a no-op when the field already points there.
# Markdown API tables for a README
banira manifest src/*.ts --md -o API.md
# write the manifest and link it from package.json
banira manifest src/*.ts -o custom-elements.json --link-packageGenerate editor IntelliSense data from the manifest so consumers get autocomplete
and hover docs for your custom elements: VS Code HTML/CSS *.custom-data.json
and a JetBrains web-types.json, all written to the output directory.
| Option | Description |
|---|---|
-o, --out-dir <dir> |
Directory to write the data files to (default .) |
banira editor-data src/*.ts -o .vscodeGenerate a self-contained .d.ts from the manifest that augments
HTMLElementTagNameMap (and, with --jsx, JSX.IntrinsicElements) so
document.querySelector('my-el') and document.createElement('my-el') are
typed for consumers — no runtime import required.
| Option | Description |
|---|---|
-o, --output <path> |
Write the .d.ts to a file instead of stdout |
--jsx |
Also augment JSX.IntrinsicElements |
banira types src/*.ts -o dist/elements.d.tsAn attribute backed by a string-literal union property (size: 'sm' | 'md' | 'lg')
is emitted as that union in the .d.ts (and as autocomplete values in the
editor-data and options in the Storybook stories), instead of a bare string.
Generate Storybook Component Story Format
files (*.stories.js) — one per component — with an argTypes controls panel
derived from each element's attributes (string-literal unions become select
options) and events (mapped to actions), plus a default story. No hand-written
story code required.
| Option | Description |
|---|---|
-o, --out-dir <dir> |
Directory to write the stories to (default .) |
--import-path <path> |
Module imported per story so the element registers (default ./{tag}.js) |
banira stories src/*.ts -o storiesPair with @storybook/web-components;
in .storybook/preview.js, call setCustomElementsManifest(manifest) (from a
banira manifest-generated custom-elements.json) for richer auto-docs.
Compare two custom-elements.json files and report API changes with a suggested
semver release type (removals/type changes → major, additions → minor,
otherwise patch). Useful as a release gate.
| Option | Description |
|---|---|
--json |
Emit the diff as JSON |
banira diff old/custom-elements.json custom-elements.jsonRecompile components whenever their source changes. Same options as compile
(-p, -o).
banira watch src/my-button.ts -o distServe a directory over HTTP with live reload (changes under the root trigger a
browser refresh). Pair it with watch for a compile-and-refresh dev loop.
Binds 127.0.0.1 only, so the server is not reachable from the network unless
you opt in with --host.
| Option | Description |
|---|---|
-p, --port <number> |
Port to listen on (default 8080) |
--host <host> |
Host/interface to bind (default 127.0.0.1; use 0.0.0.0 to expose on the network) |
--ts |
Serve TypeScript transpiled on the fly (no separate compile step) |
--hmr |
Hot-swap custom elements in place instead of full-page reload |
--import-map |
Inject a <script type="importmap"> (esm.sh) for the served modules' bare imports |
With --import-map, banira scans the served modules for bare imports, pins them
to esm.sh (version from the served root's or the project's package.json), and
injects the import map ahead of the first <script> in each served HTML page —
so bare specifiers resolve in the browser with no bundler.
With --ts, a .ts request is served as an ES module and a request for foo.js
falls back to a sibling foo.ts when no compiled foo.js exists — so you can
point a page at ./my-button.js and skip the build during development. The
served module carries an inline source map (with the original TypeScript
embedded), so breakpoints resolve to the .ts in devtools.
# terminal 1: rebuild on change
banira watch src/my-button.ts -o demo/dist
# terminal 2: serve the demo with live reload
banira serve demoThe one-command dev loop: watch and serve together, so a source edit
recompiles and the browser refreshes. The served root defaults to the output
directory.
| Option | Description |
|---|---|
-p, --project <path> |
Path to a tsconfig.json whose options override the defaults |
-o, --output <path> |
Directory to write compiled output to |
-r, --root <path> |
Directory to serve (defaults to the output dir, or .) |
--port <number> |
Port to listen on (default 8080) |
--host <host> |
Host/interface to bind (default 127.0.0.1) |
--ts |
Serve TypeScript transpiled on the fly (no separate compile step) |
banira dev src/my-button.ts -o demo/dist -r demoAudit each component against a subset of the
Gold Standard Checklist for Web Components
plus documentation-coverage of its public surface. banira mounts each element in
JSDOM and combines the shadow probe with manifest data to run independent,
id'd rules: reflection (attributes reflect to/from their property),
host-overridable (no !important on :host), and the doc-coverage rules
undocumented-event / undocumented-attribute / undocumented-part /
undocumented-slot (dispatched events, observed attributes, exposed part="…"
elements, and rendered <slot>s that lack @fires / jsdoc / @csspart /
@slot).
| Option | Description |
|---|---|
--strict |
Treat warnings as errors and exit non-zero if any finding (for CI) |
--rules <ids> |
Comma-separated rule ids to run (default: all) |
--json |
Emit findings as JSON |
Findings are warnings by default (exit 0); --strict makes them fail the build.
banira lint src/*.ts
banira lint src/*.ts --strictManifest-driven smoke test: for every custom element found in the sources,
banira compiles its module, mounts it in JSDOM, and asserts the tag registers
and upgrades to an HTMLElement — catching the most common breakages (a
component that throws on construction, or never calls customElements.define)
with no per-component test code. Exits non-zero if any element fails.
| Option | Description |
|---|---|
--reflection |
Also round-trip each observed attribute ↔ its backing property and warn on either direction that doesn't reflect |
--slots |
Also inject sample slotted content and warn on declared @slots with no matching <slot> (and shadow <slot>s with no @slot) |
--reflection and --slots are advisory: their findings print as warnings
and do not fail the command (registration failures still do), since not every
attribute is meant to reflect.
banira test src/*.ts
banira test src/*.ts --reflection --slotsScaffold a starter vanilla web component — a TypeScript source file (shadow DOM,
an observed attribute/property, an event, and the @slot / @csspart /
@cssprop / @fires jsdoc tags banira's manifest and doc tooling read) plus a
demo HTML page wired for banira serve. Existing files are left untouched
unless --force is given.
| Option | Description |
|---|---|
--force |
Overwrite existing files |
--form-associated |
Scaffold a form-associated element (static formAssociated = true + ElementInternals form/validation wiring) |
--aria |
Scaffold an ARIA role/state-reflecting element (ElementInternals.role/ariaChecked, keyboard support; records the default role in the manifest via @role) |
--hydrate |
Scaffold a component that hydrates a prerendered Declarative Shadow DOM root (adopt-or-render; no flash) |
banira init my-button src
# a checkbox-role toggle that exposes its semantics via ElementInternals
banira init my-toggle src --aria
# a component that adopts its prerendered DSD root instead of re-rendering
banira init my-card src --hydrateCompile a W3C Design Tokens (DTCG)
document into a :root CSS custom-property stylesheet. Groups become dashed
name segments (color.primary → --color-primary), $type is inherited from
parent groups, and {alias} references are resolved. Writes to stdout unless
-o is given; --selector overrides :root.
banira tokens-css design.tokens.json -o tokens.cssScaffold a theming starter: a theme.css light/dark contract (token sets via
custom properties, switched by data-theme and prefers-color-scheme), a
<theme-toggle> component that flips data-theme and persists the choice, and
a demo page. With --tokens <file> the light :root set is seeded from a DTCG
document. Existing files are left untouched unless --force is given.
| Option | Description |
|---|---|
--force |
Overwrite existing files |
--tag <tag-name> |
Tag name for the toggle component (default theme-toggle) |
--tokens <tokens.json> |
Seed the light :root token set from a DTCG document |
banira theme src/theme
banira theme src/theme --tokens design.tokens.json --tag color-scheme-switchRender the components to static HTML using
Declarative Shadow DOM
(<template shadowrootmode="open">), so they display — shadow DOM and all —
before any JavaScript runs. Components are mounted in JSDOM and their shadow
root serialized. Writes to stdout unless -o is given.
A component's adopted constructable stylesheet is inlined into the DSD template
as <style data-banira-critical> (constructable sheets aren't serialized
otherwise), so the prerendered markup is styled before JS — FOUC-free. On
hydration, hydrateShadow adopts the (deduped) sheet and drops that inline
style. Pass { inlineStyles: false } to opt out.
banira prerender src/my-button.ts -o prerendered.htmlFor programmatic SSR, createPrerenderer(files) compiles and registers the
components once and returns renderToString(tag, { attributes, children }),
which meta-frameworks (Enhance/WebC/11ty/Rocket) can call to serialize a
component to Declarative Shadow DOM on demand:
import { createPrerenderer } from 'banira';
const r = await createPrerenderer(['src/my-circle.ts']);
const html = await r.renderToString('my-circle', { attributes: { size: '40' }, children: 'Caption' });
r.close();The client half is hydrateShadow(host, { template, styles }): in a component's
connectedCallback it adopts a prerendered DSD shadow root as-is (no
re-render, no flash) when present, or creates one and renders template
otherwise — adopting the constructable stylesheet either way (DSD markup ships
without it). banira init --hydrate scaffolds a component using this pattern.
createEleventyPlugin({ files }) wires that renderer into an
Eleventy build (the role
WebC plays for 11ty): it adds a transform that
rewrites matching custom-element tags in the generated HTML into Declarative
Shadow DOM, preserving attributes and slotted children.
// .eleventy.js
import { createEleventyPlugin } from 'banira';
export default function (eleventyConfig) {
eleventyConfig.addPlugin(createEleventyPlugin({ files: ['src/my-circle.ts'] }));
}Build, test and release instructions live in DEVELOPMENT.md.
- my-circle — minimal hand-written component
MIT © Sebastian Schürmann