Symbiote.js
Why does Symbiote.js exist?
Runtime Agnostic Templates (RAT) are the main reason. In Symbiote.js, a template is an HTML string that describes markup and named bindings. It is independent of a component instance, render function, runtime, and templating tool. You can define, customize, compose, render, and reuse it at any point in the application lifecycle, on the client or the server.
RAT keeps UI structure, data bindings, and actions declarative across client rendering, SSR, SSG, and hybrid rendering. Symbiote.js can also work beside other frameworks and expose UI capabilities to agents. It requires no virtual DOM, proprietary template syntax, or mandatory build step.
Why Symbiote.js?
Symbiote.js extends native Custom Elements with reactive data bindings, portable HTML templates, state contexts, routing, and SSR. It works directly with standard browser APIs, keeps boilerplate small, and does not require a build step.
Three design choices set it apart from most UI libraries:
1. Runtime Agnostic Templates
Many component libraries bind a template to a render function, compiler, or component instance. Symbiote.js templates remain plain HTML strings with named binding metadata. The component and its data context are resolved later, when the template is rendered or hydrated. That separation makes templates portable across runtimes and rendering strategies.
- Write templates with standard tags and attributes, plus a compact binding syntax for reactivity.
- Use output from the
htmlhelper, plain HTML strings, external template modules, or document-level<template>elements. - Let the
htmlhelper turn JavaScript binding objects into HTML binding attributes. - Reuse the same template on the client, on the server, or in a static generation pipeline.
- Combine templates with any server-side template system or UI framework, including React, Vue, and Angular.
- Replace or customize a template without moving it into component logic or restructuring the rest of the application.
2. Direct extension of the DOM
Symbiote.js uses the native DOM API instead of placing a virtual DOM, JSX compiler, or proprietary component format between your code and the browser.
- Symbiote components are Custom Elements, so you can inspect and control them like any other DOM node.
- Reactive state changes update bound DOM properties directly and use the browser's own optimizations.
- Direct DOM access remains available when declarative bindings are not enough.
- The library has zero dependencies and adds little runtime weight.
- Standard HTML, CSS, and JavaScript remain the application foundation, which avoids framework lock-in.
- You can extend the application base class with project-specific features.
3. App-wide state without prop drilling
Symbiote.js uses the document structure as a data-flow graph. A component can connect to local state, its nearest owning ancestor, a shared group, or a named global context through the same binding syntax.
- Reactive bindings connect template values and action handlers to local or external data contexts.
- DOM ancestry can model data flow without prop drilling or wrapper components.
- Shared contexts group components in the same way a
nameattribute groups native radio buttons. - Named contexts provide app-wide data sources. Namespace bindings such as
APP/usergive any component or template explicit access to them. - Components can share state while remaining loosely coupled.
- The same state interface resolves values in browser and Node.js environments, so isomorphic components do not need separate state logic.
- The underlying pub/sub primitives support data flows of any complexity.
More built-in features
Itemize API
Render dynamic lists from reactive data:
<div itemize="users">
<template>
<span>{{name}}</span> -- <span>{{role}}</span>
<button ${{onclick: '^removeUser'}}>Remove</button>
</template>
</div>
- Update the source data and Symbiote.js updates the rendered list efficiently.
- Each item becomes a component connected to the corresponding data context.
- Use the
item-tagattribute to render items with a dedicated component. - CSS transitions animate removed items without additional JavaScript.
- Nest lists to represent structured data.
- Enable keyed updates for efficient reordering and fewer DOM mutations.
Server-side rendering
Use the same component implementation on the server and the client. The SSR class generates complete or streamed markup. In the browser, isoMode = true hydrates existing server-rendered content or renders the template from scratch. SSR output handles both Light DOM styles and Declarative Shadow DOM.
Built-in SPA router
The built-in router provides client-side navigation without another dependency:
- Define path-based routes with
:paramextraction. - Add route guards for authentication and access control.
- Load route components lazily.
- Bind route data directly in templates through a named routing context.
CSS-driven animations
The animateOut helper manages exit transitions without JavaScript animation code. It sets a [leaving] attribute, waits for the CSS transitionend event, and then removes the element. The Itemize API uses the same behavior for list removals.
Security
- Template writes support Trusted Types through a named
'symbiote'policy when the API is available. - Symbiote.js works with strict Content Security Policy headers.
Agentic web and TypeScript
Built-in WebMCP support lets an application expose its current UI state and actions as browser-native tools for agents.
TypeScript support
Symbiote.js combines JSDoc declarations with *.d.ts files for complex and global types. JavaScript projects get static type analysis without mandatory transpilation, while TypeScript and mixed codebases can use the same source modules without extra setup.
Ecosystem
Shadow DOM is optional, so Symbiote components can share a page with other libraries and CSS frameworks. Three.js, D3, Chart.js, and CSS utility libraries work through their usual APIs without framework adapters, wrapper components, or compatibility layers.
For teams that want a project scaffold, JSDA-Kit can create a Symbiote.js project in under a minute. It works as a static site generator for JAMstack sites or as a lightweight framework for dynamic applications. The toolkit includes SSR, esbuild bundling, automatic import maps, and a CLI that needs no configuration.
Where Symbiote.js fits
- Rich multipart widgets and complex interactive components
- Microfrontend architectures
- Reusable component libraries
- High-performance web applications
- Framework-agnostic solutions
- Meta-applications
- JAMstack and hybrid sites
- AI-ready applications
Try it
<script type="importmap">
{
"imports": {
"@symbiotejs/symbiote": "https://esm.run/@symbiotejs/symbiote"
}
}
</script>
<script type="module">
import Symbiote, { html } from '@symbiotejs/symbiote';
export class MyComponent extends Symbiote {
count: 0,
onIncrement() {
this.$.count++;
}
}
// Define template:
MyComponent.template = html`
<h2>{{count}}</h2>
<button ${{onclick: 'onIncrement'}}>Click me!</button>
`;
// Register new tag name:
MyComponent.reg('my-component');
</script>
Use the component anywhere in your HTML:
<my-component></my-component>
Save this example as HTML and open it in a browser. It works without installation, build tools, or a local server. TypeScript, bundlers, and linters remain available when your project needs them.