JSDA Stack Reference: Symbiote.js

Tests npm version npm downloads license

Symbiote.js

Symbiote.js

A lightweight, standards-first UI library built on Web Components. No virtual DOM, no compiler, no black boxes, no excess repaints. No build step required - works directly in the browser. A bundler is recommended for production performance, but entirely optional.

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

  1. Natural DOM Extension Philosophy - designed to extend platform APIs, not to replace them
  2. Runtime-Agnostic HTML Templates - outstanding flexibility for rendering strategies and further customization
  3. Powerful App-wide State Management - combine data contexts without bloated boilerplate or external tools

What's new in v3.x?

Quick start

No install needed - run this directly in a browser:

<script type="module">
  import Symbiote, { html } from 'https://esm.run/@symbiotejs/symbiote';

  class MyCounter extends Symbiote {
    count = 0;
    increment() {
      this.$.count++;
    }
  }

  MyCounter.template = html`
    <h2>{{count}}</h2>
    <button ${{onclick: 'increment'}}>Click me!</button>
  `;

  MyCounter.reg('my-counter');
</script>

<my-counter></my-counter>

Or install via npm:

npm i @symbiotejs/symbiote
import Symbiote, { html, css } from '@symbiotejs/symbiote';

Core concepts

Reactive state

class TodoItem extends Symbiote {
  text = '';
  done = false;
  toggle() {
    this.$.done = !this.$.done;
  }
}

TodoItem.template = html`
  <span ${{onclick: 'toggle'}}>{{text}}</span>
`;

State changes update the DOM synchronously. No virtual DOM, no scheduling, no surprises. And since components are real DOM elements, state is accessible from the outside via standard APIs:

document.querySelector('my-counter').$.count = 42;

This makes it easy to control Symbiote-based widgets and microfrontends from any host application - no framework adapters, just DOM.

Templates

Templates are plain HTML strings - runtime-agnostic, easy to test, easy to move between files:

// Separate file: my-component.template.js
import { html } from '@symbiotejs/symbiote';

export default html`
  <h1>{{title}}</h1>
  <button ${{onclick: 'doSomething'}}>Go</button>
`;

The html function supports two interpolation modes:

Itemize (dynamic reactive lists)

Render lists from data arrays or objects with efficient updates:

class TaskList extends Symbiote {
  tasks = [
    { name: 'Buy groceries' },
    { name: 'Write docs' },
  ];
}

TaskList.template = html`
  <ul itemize="tasks">
    <template>
      <li>{{name}}</li>
    </template>
  </ul>
`;

Pop-up binding (^)

The ^ prefix works in any nested component template - it walks up the DOM tree to find the nearest ancestor that has the property registered in its data context (init$ or add$()):

<!-- Text binding to parent property: -->
<div>{{^parentTitle}}</div>

<!-- Handler binding to parent method: -->
<button ${{onclick: '^parentHandler'}}>Click</button>

Named data contexts

Share state across components without prop drilling:

import { PubSub, html } from '@symbiotejs/symbiote';

PubSub.registerCtx({
  user: 'Alex',
  theme: 'dark',
}, 'APP');

// Any component can read/write:
this.$['APP/user'] = 'New name';

// Any template can use property directly:
let template = html`<h2>{{APP/user}}</h2>`;

Shared context (*)

Inspired by native HTML name attributes - like how <input name="group"> groups radio buttons - the ctx attribute groups components into a shared data context. Components with the same ctx value share *-prefixed properties:

<upload-btn ctx="gallery"></upload-btn>
<file-list  ctx="gallery"></file-list>
<status-bar ctx="gallery"></status-bar>
class UploadBtn extends Symbiote {
  init$ = { '*files': [] }

  onUpload() {
    this.$['*files'] = [...this.$['*files'], newFile];
  }
}

class FileList extends Symbiote {
  init$ = { '*files': [] }
}

class StatusBar extends Symbiote {
  init$ = { '*files': [] }
}

All three components access the same *files state - no parent component, no prop drilling, no global store boilerplate. Just set ctx="gallery" in HTML and use *-prefixed properties. This makes it trivial to build complex component relationships purely in markup, with ready-made components that don't need to know about each other.

Application routing

// Import optional module:
import { AppRouter } from '@symbiotejs/symbiote/core/AppRouter.js';

AppRouter.initRoutingCtx('R', {
  home:    { pattern: '/' },
  profile: { pattern: '/user/:id' },
  about:   { pattern: '/about', lazyComponent: () => import('./about.js') },
});

CSS Styling

Shadow DOM is optional in Symbiote - use it when you need isolation, skip it when you don't. This gives full flexibility:

Light DOM - style components with regular CSS, no barriers:

MyComponent.rootStyles = css`
  my-component {
    display: flex;
    gap: 1rem;

    & button { color: var(--accent); }
  }
`;

This style will be applied to nearest upper shadow root, if exists and to common document if not.

Shadow DOM - opt-in isolation when needed:

class Isolated extends Symbiote {}

Isolated.shadowStyles = css`
  :host { display: block; }
  ::slotted(*) { margin: 0; }
`;

All native CSS features work as expected: CSS variables flow through shadow boundaries, ::part() exposes internals, modern nesting, @layer, @container - no framework abstractions in the way. Mix light DOM and shadow DOM components freely in the same app.

CSS Data

Components can read CSS custom property values to initiate reactive state:

my-widget {
  --label: 'Click me';
}
class MyWidget extends Symbiote {...}

MyWidget.template = html`
  <span>{{--label}}</span>
`;

Best for

Docs & Examples

Questions or proposals? Welcome to Symbiote Discussions! ❤️


© rnd-pro.com - MIT License

arrow_forward Template Repoarrow_forward JSDA Manifestarrow_forward Symbiote.jsarrow_forward Cloud Images Toolkit
chevron_right