Symbiote.js

Symbiote.js

Tired of pointless re-renders? Bloated bundles? Chaotic data flow? Laggy UI? Magic abstractions and compiler quirks? Done with frameworks that add complexity instead of removing it?

Miss the vanilla flavor, but still want great DX?

Symbiote.js is here to save you

Documentation Code Playground npm GitHub Community

So, what's the big idea?

Symbiote.js isn't trying to be yet another frontend library. Think of it more like a philosophy for web development that embraces the power of what's already built into your browser.

We believe in using modern, standard web technologies, not reinventing them. Symbiote.js simply adds a touch of modern developer experience (DX) on top of vanilla Custom Elements. You get reactive data bindings, flexible HTML-based templates, powerful state management, computed properties, built-in routing, SSR support, and easily extendible components — all with minimal boilerplate and zero build-step requirement.

Here are the three most important differences between Symbiote.js and other frameworks:

1. Natural DOM Extension Philosophy

No opaque abstractions. Unlike frameworks that wrap your code in complex abstractions (virtual DOM, JSX compilers, proprietary syntax), Symbiote.js works directly with the native DOM API. For you, this means:

  • No Black Boxes: What you write is what the browser runs. No mandatory compilation steps, no additional magic.
  • Native Speed: It leverages the browser's own optimizations.
  • No Framework Lock-in: It plays nicely with any other web technology you want to use.
  • Feather-Light: At only ~6kb (br/gzip), it won't weigh your pages down.
  • Zero Dependencies: It's entirely built on native web platform features.
  • Real DOM Elements: Symbiote components are actual Custom Elements, so you can interact with them just like any other DOM node.
  • The Classic Trio: Just HTML, CSS, and JavaScript. Everything you need for solid, enterprise-ready work.
  • Use the DOM API When You Need It: It's no longer considered an anti-pattern.
  • Extensible: Easily add any features you need to your application's base class.

2. Runtime-Agnostic HTML Templates

Symbiote.js offers outstanding flexibility in how you customize and compose your UI. Templates are just standard HTML strings that can be defined or reused anywhere - client-side or server-side:

  • Just HTML: Use standard tags and attributes with a simple binding syntax for reactivity.
  • html Helper Function: Automatically transforms JS object declarations into HTML binding attributes.
  • Plays Well With Others: Works seamlessly with any templating system or server-side technology.
  • Loosely Coupled: More flexibility for customizations. One change doesn't break everything.
  • Ready for SSR: A perfect fit for server-side rendering (SSR) and static site generation (SSG).
  • Integrates with Anything: Easily use it alongside React, Vue, Angular, or any other framework.
// Define templates anywhere, client or server:
import html from '@symbiotejs/symbiote/core/html.js';

const myTemplate = html`
  <div class="my-component" ${{onclick: 'showBubble'}}>
    <h1>{{title}}</h1>
    <div>{{content}}</div>
  </div>
`;

3. App-wide State Management

Symbiote.js offers a fresh take on how components communicate and manage state:

  • Powerful Data-Binding Syntax: Connect data and action handlers from any local or external data context with minimal boilerplate.
  • Natural Data Flow: Leverage the DOM structure itself to naturally model your application's state.
  • Compose with Ease: Components can share common properties effortlessly, much like native radio buttons. No prop-drilling or extra wrappers required.
  • Loosely Coupled State: Components can share state without being tightly bound to one another.
  • Global Named Data Contexts: Create app-wide data sources that any component can access by name prefix.
  • Isomorphic State Management: Access data values on the server and the client in the exact same way, without any additional component logic.
  • Simple Pub/Sub Pattern: Use fundamental data-bricks to build robust data flows of any complexity.
// Pop-up data context (^) - bind directly to some DOM-tree's ancestor component's state
html`<button ${{onclick: '^onButtonClicked'}}>Click me!</button>`;

// Explicitly shared data context (*)
html`<div>{{*sharedProperty}}</div>`;

// Global named data context (/)
html`<div>{{APP/someProperty}}</div>`;

Other Features

Itemize API

Render dynamic lists declaratively — no manual DOM manipulation, no key prop juggling. Just point itemize at a data array or object and define an inline <template> for each item:

<div itemize="users">
  <template>
    <span>{{name}}</span><span>{{role}}</span>
    <button ${{onclick: '^removeUser'}}>Remove</button>
  </template>
</div>
  • Each item - is a component: Created automatically and connected to the proper data context.
  • Custom items: Use item-tag attribute to delegate rendering to a dedicated component for complex list items.
  • Auto-animated removals: Items with CSS transitions animate out automatically via the [leaving] attribute — zero JS needed.
  • List nesting: Nested lists are supported for complex tree-like structured data.
  • Optional keyed updates: Keyed processor for efficient reordering and minimal DOM mutations.

Server Side Rendering

Use same-code isomorphic components on server and client. Just one flag isoMode = true for efficient hydration. One SSR class for server-side markup generation. Streaming is also supported. Light DOM styles and Declarative Shadow DOM — all handled automatically during SSR output.

SSR — simpler than ever before

Built-in SPA Router

Full-featured client-side routing, right out of the box — no extra dependencies needed:

  • Path-based routes with :param extraction
  • Route guards for authentication and access control
  • Lazy loading of route components
  • Named routing context — bind route data directly in templates

CSS-Driven Animations

Exit transitions with zero JS animation code. The animateOut helper sets a [leaving] attribute, waits for CSS transitionend, then removes the element. Works automatically with the Itemize list rendering API.

Enterprise-Grade Security

  • Trusted Types compatible — template writes use a named 'symbiote' policy when the API is available.
  • Full CSP compliance — works with the strictest Content Security Policy headers.

TypeScript Support

Symbiote.js leverages a JSDoc declaration approach alongside *.d.ts files for complex and global types. This allows you to have robust type security without a mandatory transpilation step. It also enables you to seamlessly use the source code module-by-module in either JavaScript or TypeScript projects with zero additional setup.

Ecosystem

Symbiote.js stays close to the platform, so it works seamlessly with any popular library or CSS framework - there is no forced Shadow DOM isolating your components from the rest of the page. Need Three.js for 3D, D3 or Chart.js for data visualization, or any CSS utility library? Just use them as you normally would - no adapters, no wrappers, no compatibility layers.

For those who want a batteries-included starting point, there is JSDA-Kit - a ready-made toolkit built around Symbiote.js that lets you spin up a new web project in under a minute. JSDA-Kit works as a static site generator (SSG) for JAMStack sites, or as a lightweight framework for more complex dynamic applications - with built-in SSR, esbuild-powered bundling, automatic import maps, and a zero-config CLI. One scaffold command and you're up and running.

Symbiote.js vs React / Next.js vs Lit

Let's highlight the key differences with some popular options:

Symbiote.jsReact / Next.jsLit
Size~6 KB gzip~44 KB (React+DOM), Next.js adds much more~16 KB gzip
Virtual DOMNone — direct DOMVirtual DOM + diffingNone — direct DOM
Compiler / BuildNot requiredRequired (JSX, bundler)Not required
StandardNative Web ComponentsProprietary component modelNative Web Components
TemplatesContext-less HTML stringsJSX (JS only)Tagged template literals (JS only)
SSRBuilt-in, streamingNext.js (complex setup)Limited (@lit-labs/ssr)
RoutingBuilt-in (SPA)Next.js filesystem routingNot included
State ManagementBuilt-in PubSub + contextsExternal (Redux, Zustand, etc.)Not included
Computed PropsAuto-trackeduseMemo (manual deps)Manual
CSS ScopingLight DOM + Shadow DOMCSS Modules / CSS-in-JSShadow DOM only
Framework Lock-inNoneHighLow
CSP / Trusted TypesBuilt-inDepends on setupPartial
Dependencies0Many1 (lit-html)

Why not React? React requires a build pipeline, proprietary JSX syntax, a virtual DOM layer that adds overhead, and external libraries for routing, state, and SSR. A full Next.js stack is hundreds of kilobytes. Symbiote.js gives you all of that in ~6 KB with zero lock-in.

Why not Lit? Lit is a solid Web Components library, but it lacks built-in routing, state management, SSR streaming, and computed properties. It forces Shadow DOM for styling, which can be limiting. Symbiote.js offers more batteries-included features at a smaller footprint, plus unique concepts like DOM-based data context and CSS Data binding.

Where It Really Shines

  • Building rich multipart widgets
  • Crafting complex interactive components
  • Micro-frontend architectures
  • Creating reusable component libraries
  • High-performance web apps
  • Framework-agnostic solutions
  • Meta-applications
  • JamStack / Hybrid sites

The Core Goodness

  • Loosely Coupled Architecture
  • Ultralight: Just ~6kb (br/gzip)
  • Wickedly Fast: Native DOM performance, no virtual DOM overhead
  • Computed Properties: Auto-tracked, microtask-batched
  • Memory Savvy: No wasteful immutable data structures
  • Enterprise-Ready: Full CSP + Trusted Types compliance
  • Type-Safe: Full TypeScript support
  • Built-in Router: Path-based routing, guards, lazy loading
  • SSR Out of the Box: Server rendering, streaming, hydration
  • Reactive CSS Data: CSS custom properties as reactive state
  • CSS-Driven Animations: Exit transitions with zero JS code
  • Dev Mode: Verbose warnings for debugging with zero production overhead
  • Plays Well Everywhere: Works with any stack
  • Tidy: Automatic cleanup with configurable destruction delay
  • Open Source: MIT licensed, of course!

Give It a Spin

<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 {

    // Initialize state:
    init$ = {
      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>

This little HTML example has everything you need to get a Symbiote.js app running. No build tools, no installation, no local server — just open it in a browser. And of course, you can use it with TypeScript, bundlers, linters, and all that good stuff.

Get your 100 points in Lighthouse

Live Code Playground



More examples

Want to Learn More?

Documentation npm GitHub Community
03.05.2026
Symbiote VS Lit
David and Goliath: differences, pros and cons...
11.12.2025
JSDA is very simple
A new, simple, but powerful way to build modern web applications.
28.09.2024
It was really possible?
Symbiote.js as an answer to many questions
17.09.2024
Smart HTML-tags
Simple recipe with the Artificial Intelligence
18.01.2024
Symbiote.js 2.x
Next generation of Symbiote.js is released. Let's see what's new...
RND-PRO.com © 2026