Symbiote.js v3.7.x
Recent Symbiote.js updates focused on one of the library's core ideas: templates should stay portable, simple, and close to the platform.
Symbiote templates are not instance-bound render functions. They are runtime-agnostic HTML strings that can come from the html helper, plain strings, external modules, SSR output, or DOM <template> elements. That design gives Symbiote a different shape from many component libraries: representation can stay separate from component logic.
Self-Closing Custom Elements
HTML custom elements are not void elements, so the safest form is always explicit:
html`<my-component></my-component>`;
For developer convenience, the html helper now normalizes empty custom element syntax:
html`<my-component />`;
This outputs:
<my-component></my-component>
This makes authoring nested component templates a little more forgiving while still producing clear HTML output.
Faster html() Helper
The html() helper was also optimized.
A local microbenchmark for 200k calls showed improvements across the main cases:
plain: 19.42ms -> 4.11ms
bindings: 47.74ms -> 31.37ms
custom: 56.64ms -> 32.26ms
The public output contract stays the same, but the common path is cheaper.
Template Documentation: Runtime-Agnostic by Design
The template documentation now opens with the most important architectural point: Symbiote templates are runtime and context agnostic.
They do not execute inside a component instance and do not close over this. They describe markup and named bindings. The actual component instance, state, parent context, shared context, named context, or CSS data source is resolved later when the template is rendered or hydrated.
That makes templates reusable, replaceable, SSR-friendly, and easy to define outside component classes.
TypeScript and the Hybrid Approach
A new TypeScript guide explains how Symbiote balances static typing with runtime-agnostic templates.
Symbiote uses JSDoc and generated .d.ts files for component APIs, state, helpers, and editor support. But template bindings are intentionally portable strings. A template may be reused with different components or loaded from external HTML, so TypeScript cannot always prove every binding relationship at declaration time.
The recommended model is hybrid:
- Use TypeScript or JSDoc for component state, methods, helpers, and APIs.
- Use Symbiote runtime diagnostics for template binding validation when the template meets the actual component context.
For stricter projects, allowTemplateInits = false prevents template bindings from silently creating missing local properties.
Typed Binding Objects
The TypeScript guide also documents a practical pattern for stronger checks in templates: move binding maps into explicitly typed objects.
type BindingMap<El, Ctx> = Partial<Record<
Extract<keyof El, string>,
Extract<keyof Ctx, string>
>>;
type CounterTemplateCtx = {
count: number;
label: string;
increment: () => void;
};
const incrementBtn = {
onclick: 'increment',
textContent: 'label',
} satisfies BindingMap<HTMLButtonElement, CounterTemplateCtx>;
MyCounter.template = html`
<button ${incrementBtn}>Click me!</button>
`;
This lets TypeScript check both sides:
- keys are real element properties, such as
onclickortextContent - values are real state or context keys, such as
incrementorlabel
Typos like onclik or lable are caught statically, while final runtime context matching remains covered by Symbiote's development diagnostics.
Summary
These changes improve Symbiote in three connected ways:
- cleaner HTML output for custom elements
- faster template helper performance
- clearer documentation for runtime-agnostic templates and TypeScript workflows
The result keeps Symbiote close to the platform while making everyday authoring smoother, safer, and easier to reason about.