# 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? - **WebMCP support** - expose live Symbiote UI actions as browser-native tools for agents. See the [WebMCP docs](https://github.com/symbiotejs/symbiote.js/blob/webmcp/docs/webmcp.md). - **Server-Side Rendering** - render components to HTML with `SSR.processHtml()` or stream chunks with `SSR.renderToStream()`. Client-side hydration via `ssrMode` attaches bindings to existing DOM without re-rendering. - **Isomorphic components** - `isoMode` flag makes components work in both SSR and client-only scenarios automatically. If server-rendered content exists, it hydrates; otherwise it renders the template from scratch. One component, zero conditional logic. - **Computed properties refined** - reactive derived state with microtask batching. - **Path-based router** - optional `AppRouter` module with `:param` extraction, route guards, and lazy loading. - **Exit animations** - `animateOut(el)` for CSS-driven exit transitions, integrated into itemize API. - **Dev mode** - `Symbiote.devMode` enables verbose warnings; import `devMessages.js` for full human-readable messages. - **DSD hydration** - `ssrMode` supports both light DOM and Declarative Shadow DOM. - **Class property fallback** - binding keys not in `init$` fall back to own class properties/methods. - **Lazy mode** - `lazyMode` flag defers component initialization and rendering based on viewport visibility. Can also be enabled via the `lazy` attribute on `itemize` containers to efficiently handle massive data sets. ## Quick start No install needed - run this directly in a browser: ```html ``` Or install via npm: ```bash npm i @symbiotejs/symbiote ``` ```js import Symbiote, { html, css } from '@symbiotejs/symbiote'; ``` ## Core concepts ### Reactive state ```js class TodoItem extends Symbiote { text = ''; done = false; toggle() { this.$.done = !this.$.done; } } TodoItem.template = html` {{text}} `; ``` 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: ```js 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: ```js // Separate file: my-component.template.js import { html } from '@symbiotejs/symbiote'; export default html`

{{title}}

`; ``` The `html` function supports two interpolation modes: - **Object** → reactive binding: `${{onclick: 'handler'}}` - **String/number** → native concatenation: `${pageTitle}` ### Itemize (dynamic reactive lists) Render lists from data arrays or objects with efficient updates: ```js class TaskList extends Symbiote { tasks = [ { name: 'Buy groceries' }, { name: 'Write docs' }, ]; } TaskList.template = html` `; ``` ### 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$()`): ```html
{{^parentTitle}}
``` ### Named data contexts Share state across components without prop drilling: ```js 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`

{{APP/user}}

`; ``` ### Shared context (`*`) Inspired by native HTML `name` attributes - like how `` groups radio buttons - the `ctx` attribute groups components into a shared data context. Components with the same `ctx` value share `*`-prefixed properties: ```html ``` ```js 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 ```js // 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: ```js 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: ```js 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: ```css my-widget { --label: 'Click me'; } ``` ```js class MyWidget extends Symbiote {...} MyWidget.template = html` {{--label}} `; ``` ## Best for - **Complex widgets** embedded in any host application - **Low-code HTML-based solutions** - simple declarative everything - **Micro frontends** - standard custom elements, no framework coupling - **Reusable component libraries** - works in React, Vue, Angular, or plain HTML - **SSR-powered apps** - lightweight server rendering without framework lock-in - **Framework-agnostic solutions** - one codebase, any context - **Modern AI-first web** - expose the application state to WebMCP tools automatically ## Docs & Examples - [Documentation](https://github.com/symbiotejs/symbiote.js/blob/main/docs/README.md) - [Live Examples](https://rnd-pro.com/symbiote/3x/examples/) - Interactive Code Playground - [JSDA-Kit](https://github.com/rnd-pro/jsda-kit) - All-in-one companion tool: server, SSG, bundling, import maps, and native Symbiote.js SSR integration - [AI / llms.txt](https://rnd-pro.com/symbiote/llms.txt) — index for AI tools - [Full docs (single file)](https://rnd-pro.com/symbiote/llms-full.txt) — complete merged reference for AI context ## Related articles - [Symbiote.js: superpowers for Web Components](https://dev.to/foxeyes/symbiotejs-superpowers-for-web-components-1gid) - [Symbiote.js: v3 highlights](https://dev.to/foxeyes/symbiotejs-v3-web-components-with-ssr-in-6kb-10n6) - [Symbiote.js vs Lit](https://dev.to/foxeyes/lit-vs-symbiotejs-22gj) - [JSDA Stack - A Revolutionary Simple Approach to Build Modern Web](https://dev.to/foxeyes/jsda-kit-a-revolutionary-simple-approach-to-build-modern-web-1dip) **Questions or proposals? Welcome to [Symbiote Discussions](https://github.com/symbiotejs/symbiote.js/discussions)!** ❤️ --- © [rnd-pro.com](https://rnd-pro.com) - MIT License --- # Animations Symbiote.js provides CSS-driven exit transitions with zero JS animation code. ## `animateOut` `animateOut(el)` sets the `[leaving]` attribute on an element, waits for CSS `transitionend`, then removes the element from the DOM. If no CSS transition is defined, removes immediately. ```js import { animateOut } from '@symbiotejs/symbiote'; // or as a static method: Symbiote.animateOut(el); ``` ## CSS pattern Use `@starting-style` for enter animations and `[leaving]` for exit: ```css my-item { opacity: 1; transform: translateY(0); transition: opacity 0.3s, transform 0.3s; /* Enter (CSS-native, no JS needed): */ @starting-style { opacity: 0; transform: translateY(20px); } /* Exit (triggered by animateOut): */ &[leaving] { opacity: 0; transform: translateY(-10px); } } ``` ## Itemize integration Itemize processors use `animateOut` automatically for item removal. Items with CSS `transition` + `[leaving]` styles will animate out before being removed from the DOM: ```css user-card { opacity: 1; transition: opacity 0.3s; @starting-style { opacity: 0; } &[leaving] { opacity: 0; } } ``` No additional JavaScript is required — just define the CSS transitions and Symbiote handles the rest. --- # Attributes Like all regular DOM elements, Symbiote components can have their own HTML attributes. And like standard Custom Elements, they can react to dynamic attribute changes. ## Attribute connection The simplest way to connect a local state property to an attribute value is with the `@` token: ```js class MyComponent extends Symbiote { init$ = { '@attribute-name': 'initial value', } } ``` Or initiate from the component's template directly: ```js class MyComponent extends Symbiote {} MyComponent.template = html`

{{@attribute-name}}

`; ``` Then use it as an component's attribute in markup: ```html ``` ## Attribute change reaction Using a property accessor: ```js class MyComponent extends Symbiote { set 'my-attribute'(val) { console.log(val); } } MyComponent.observedAttributes = [ 'my-attribute', ]; ``` ## `bindAttributes()` static method Reflect attribute values to property values: ```js class MyComponent extends Symbiote { init$ = { myProp: '', } } // Map the attribute name to corresponding property key: MyComponent.bindAttributes({ 'my-attribute': 'myProp', }); // observedAttributes is auto-populated MyComponent.template = html`
{{myProp}}
`; ``` ## Reserved attribute names These attribute names are used internally by Symbiote.js: - `bind` - `ctx` - `ref` - `itemize` - `item-tag` - `use-template` - `skip-text-nodes` --- # Common Mistakes Patterns that frequently trip up new users and AI code generators. --- ## 1. Using `this` inside template strings Templates are context-free strings — they DO NOT execute inside a component instance and do not close over `this`. Binding values by name; the component resolves them at render time. ```js // WRONG MyComponent.template = html`
${this.title}
`; // CORRECT — use binding syntax MyComponent.template = html`
{{title}}
`; // OR MyComponent.template = html`
`; // OR MyComponent.template = `
`; ``` --- ## 2. Defining `template`, `rootStyles`, or `shadowStyles` inside the class body `template`, `rootStyles`, and `shadowStyles` are **static property setters** on the `Symbiote` base class — not regular static fields. Assigning them inside the class body with `static template = ...` bypasses the setter and does nothing. ```js // WRONG class MyComponent extends Symbiote { static template = html`
{{text}}
`; // setter never called } // CORRECT — assign outside the class body class MyComponent extends Symbiote {} MyComponent.template = html`
{{text}}
`; MyComponent.rootStyles = css`my-component { display: block; }`; ``` --- ## 3. Missing `^` prefix when referencing a parent-defined handler from an item template Itemize items are real Symbiote components with their own reactive state (the data properties mapped from the source array or object). They can have their own handlers too — either passed as functions in the source data objects, or defined on a separate component class registered for the tag name set by `item-tag` attribute. The common mistake is defining a shared handler on the **parent** (e.g. to handle all item clicks in one place) and then binding to it from the item template *without* `^`. Without `^`, the binding looks for the handler on the item itself — where it doesn't exist — and fails. ```js class MyList extends Symbiote { init$ = { items: [{ name: 'Alice' }, { name: 'Bob' }], onItemClick: (e) => { console.log('clicked'); }, } } // WRONG — looks for onItemClick on the item, not the parent MyList.template = html` `; // CORRECT — ^ walks up the DOM to find onItemClick in the parent's init$ MyList.template = html` `; ``` > `^`-targeted properties must be defined in the ancestors's `init$` — the walk does not check plain class properties. When each item needs its own independent handler, you can pass it directly in the data: ```js this.$.items = [ { name: 'Alice', onItemClick: () => console.log('Alice clicked') }, { name: 'Bob', onItemClick: () => console.log('Bob clicked') }, ]; ``` ```html ``` Or, as it recommended for the most cases, register a dedicated component class for the tag name set by `item-tag`: ```js class MyItem extends Symbiote { onItemClick() { console.log(this.$.name); } } MyItem.template = html`
  • `; MyItem.reg('my-item'); ``` ```html ``` --- ## 4. Using `@` attribute prefix directly in HTML `@` is a binding syntax prefix only — it means "bind to HTML attribute" inside `${{}}` expressions. It is not a valid HTML attribute prefix. ```html
    ...
    ...
    ...
    ``` --- ## 5. Expecting dotted `init$` keys to create nested state State is flat by design. A key like `'obj.prop'` in `init$` is a literal flat key named `obj.prop` — it does not create a nested object `{ obj: { prop: ... } }`. `this.$['obj.prop']` reads and writes that same flat key and works fine. ```js // MISLEADING — this does NOT create { obj: { prop: 'value' } } init$ = { 'obj.prop': 'value' }; // It creates a flat key: store['obj.prop'] = 'value' // Reading/writing it works: this.$['obj.prop'] = 'new' ✓ // If you need a nested object in state, store it as a flat key with an object value: init$ = { obj: { prop: 'value' } }; this.$.obj = { ...this.$.obj, prop: 'new' }; // replace the whole object to trigger reactivity ``` Dot notation in binding **targets** is fully supported — for both standard DOM properties and child component state: ```js // DOM property path: html`
    Text
    ` // Child component's $ state proxy: html`` ``` --- ## 6. Using `*prop` without a `ctx` attribute or `--ctx` CSS variable Shared context properties (`*`-prefix) require a context name to be set on the component — either via the `ctx` HTML attribute or the `--ctx` CSS custom property. Without one, `*` props silently have no effect (dev mode warns). ```html ``` --- ## 7. Expecting Shadow DOM by default Shadow DOM is opt-in. By default, templates render into the component's light DOM. Use `renderShadow = true` or assign `shadowStyles` to opt in. ```js // Light DOM (default) class MyComponent extends Symbiote {} // Shadow DOM — either flag: class MyComponent extends Symbiote { renderShadow = true; } // or via shadowStyles (auto-creates shadow root): MyComponent.shadowStyles = css`:host { display: block; }`; ``` --- ## 8. Adding a wrapper div inside the template The custom element itself is the container. Adding a wrapping `
    ` as the single root inside the template is unnecessary — it creates extra DOM nesting and duplicates the element's own box. Style the component tag directly instead. ```js // WRONG — the
    is redundant, my-widget is already the root MyWidget.template = html`

    {{title}}

    `; // CORRECT — template content renders directly inside the custom element MyWidget.template = html`

    {{title}}

    `; ``` ```css /* Style the element tag directly */ my-widget { display: block; padding: 20px; } ``` --- ## 9. Relying on class property fallbacks for prefixed bindings Class property fallbacks (resolving unregistered keys from own instance properties or prototype methods) only apply to **local, unprefixed** bindings. Any prefixed binding — `^prop`, `*prop` — resolves exclusively through the data context (`init$` or `add$()`). Plain class properties are never checked for these. ```js // WRONG — onItemClick is a class property, not in init$ // ^ will not find it class TaskList extends Symbiote { onItemClick() { console.log('clicked'); } } TaskList.template = html` `; // CORRECT — register in init$ so any prefixed binding can resolve it class TaskList extends Symbiote { init$ = { onItemClick: () => { console.log('clicked'); }, } } ``` The same applies to shared (`*`) bindings — the property must exist in the shared context's registered data, not merely as a class property. Named external contexts (`CTX/prop`) are different: they are registered globally via `PubSub.registerCtx()` and are fully independent of any component's `init$` — components reference them freely without declaring anything locally. --- ## 10. Using `{{prop}}` binding syntax inside tag definitions `{{prop}}` is a **text node** binding — it only works inside element content. Any binding that targets a tag itself — attributes, DOM properties, event handlers, or CSS custom properties — must use `${{}}` or `bind=""` syntax inside the opening tag. ```html
    ...
    ...
    ...
    ...

    Hello, {{userName}}!

    Theme: {{APP/theme}}

    ``` --- ## 11. Expecting two-way data binding Symbiote.js bindings are **one-way by design** — from state to DOM. There is no `v-model`, `[(ngModel)]`, or `bind:value` equivalent. To react to user input, wire the event handler explicitly and write back to state yourself. ```js // WRONG — expecting the binding to also update state on user input MyComponent.template = html` `; // CORRECT — handle the input event and write back to state class MyComponent extends Symbiote { init$ = { query: '' } } MyComponent.template = html` `; ``` ```js class MyComponent extends Symbiote { init$ = { query: '', onInput: (e) => { this.$.query = e.target.value; }, } } ``` This is intentional — explicit event handling keeps data flow predictable and avoids the hidden side effects that two-way binding can introduce. --- ## 12. Treating `init$` as a plain object `init$` is processed once at connection time to populate the component's reactive context. Mutating it after the fact has no effect. Use `this.$` or `add$()` for runtime changes. ```js // WRONG — modifying init$ after construction does nothing class MyComponent extends Symbiote { init$ = { count: 0 }; initCallback() { this.init$.count = 10; // too late, already processed } } // CORRECT class MyComponent extends Symbiote { init$ = { count: 0 }; initCallback() { this.$.count = 10; // write through the $ proxy } } ``` --- # Context Context is the central concept in Symbiote.js. Rather than passing data through prop chains or maintaining a separate global store, Symbiote uses the DOM structure itself as the data flow graph. Every component can read from and write to multiple data sources - its **context** is the union of all the sources currently accessible to it. There are seven context types, each addressed by a token prefix in property keys: | Token | Context Type | Scope | |-------|-------------|-------| | _(none)_ | [Local](#local-context) | Component instance | | `^` | [Pop-up](#pop-up-context) | Nearest ancestor that owns the property | | `*` | [Shared](#shared-context) | All components sharing a `ctx` attribute | | `name/` | [Named](#named-context) | Any component, anywhere | | `--` | [CSS Data](#css-data-context) | Inherited from the CSS cascade | | `@` | [Attribute](./attributes.md) | HTML attribute on the element | | `+` | [Computed](./properties.md#computed-properties) | Derived, auto-recalculated value | --- ## Local context Local context is the component's own reactive state, scoped to the instance. It works the same way as component state in other frameworks and is invisible to other components. Define properties in `init$`: ```js class MyComponent extends Symbiote { init$ = { myProperty: 'some value', } } ``` Use the `$` proxy to read and write values at runtime: ```js class MyComponent extends Symbiote { init$ = { myProperty: 'some value', } renderCallback() { console.log(this.$.myProperty); // > 'some value' this.$.myProperty = 'new value'; } } MyComponent.template = html`

    {{myProperty}}

    `; ``` For simple components without shared or computed props, you can also declare properties as plain class fields - Symbiote picks them up automatically via class-field fallback: ```js class MyComponent extends Symbiote { count = 0; label = 'Hello'; } ``` > Use `init$` when you need shared (`*`), computed (`+`), or attribute (`@`) props in the same declaration - those tokens are only recognized inside `init$`. See [Properties →](./properties.md) for the full property API: `add$()`, `sub()`, `set$()`, computed props, and more. --- ## Named context Named context is a global, named data store created independently of any component. Any component can read from or write to it using the `CONTEXT_NAME/property` syntax - regardless of DOM position. Use named context when data must be shared across unrelated parts of the application. **Creating a named context:** ```js import { PubSub } from '@symbiotejs/symbiote'; let appCtx = PubSub.registerCtx({ theme: 'light', user: null, }, 'APP'); ``` **Accessing it from a component:** ```js class MyComponent extends Symbiote { init$ = { 'APP/theme': 'light', // optional local fallback - used if the named context hasn't published yet } renderCallback() { console.log(this.$['APP/theme']); // read this.$['APP/theme'] = 'dark'; // write - updates the named context for all subscribers } } MyComponent.template = html`
    ...
    `; ``` **Example - localization:** ```js import Symbiote, { html, PubSub } from '@symbiotejs/symbiote'; let l10nCtx = PubSub.registerCtx({ users: 'Users', comments: 'Comments', likes: 'Likes', }, 'L10N'); MyComponent.template = html`
    {{L10N/users}} - {{numberOfUsers}}
    {{L10N/comments}} - {{numberOfComments}}
    {{L10N/likes}} - {{numberOfLikes}}
    `; // Switch language at any time - all subscribed components update instantly: l10nCtx.multiPub({ users: 'Usuarios', comments: 'Comentarios', likes: 'Gustos', }); ``` You can also read and modify named context directly from any component using the `$` proxy: ```js class MyComponent extends Symbiote { renderCallback() { console.log(this.$['L10N/users']); this.$['L10N/users'] = 'ユーザー'; } } ``` More information about `PubSub` in the [PubSub →](./pubsub.md) section. --- ## Pop-up context Pop-up context lets a child component reach a property defined by an ancestor, without the ancestor needing to pass it down explicitly. Symbiote walks up the DOM tree until it finds a component that has the requested property in its data context. Use the `^` token to reference a pop-up property - in both text bindings and event handlers: ```html
    {{^parentTitle}}
    ``` ```js class MyButton extends Symbiote {} MyButton.template = html` {{^parentTitle}} `; ``` Symbiote walks up the DOM tree until it finds a component with the requested property registered in its context: ```js class MyEditor extends Symbiote { init$ = { onButtonClicked: () => console.log('clicked'), editorTitle: 'My Editor', } } ``` > [!IMPORTANT] > Pop-up lookup only searches the **data context** (properties registered via `init$` or `add$()`). Plain class properties are not resolved this way. Always declare `^`-targeted properties in the parent's `init$`: > ```js > class ParentComponent extends Symbiote { > init$ = { > onButtonClicked: () => console.log('clicked'), > parentTitle: 'Hello', > } > } > ``` Pop-up context is useful for composition - the same child component adapts to whichever ancestor provides the expected behavior: ```js html` `; // or: html` `; ``` > Like the CSS cascade, pop-up context has no collision guard. Use additional prefixes (e.g. `myApp_onSave`) in environments you don't fully control. --- ## Shared context Shared context is inspired by native HTML `name` attributes - the same way `` connects radio buttons into one workflow, the `ctx` attribute connects Symbiote components into a shared data context. Components with the same `ctx` name access a common reactive store with no intermediary component required. **Assign a context name via the `ctx` HTML attribute:** ```html ``` **Or via the `--ctx` CSS custom property**, which cascades like any CSS variable: ```css .gallery-section { --ctx: gallery; } ``` ```html ``` The CSS approach is useful when: - You want **layout-driven grouping** - components inherit context from their visual container rather than repeating the attribute on each one - You need to **override context at different DOM levels** - just like any CSS custom property, `--ctx` cascades and can be reassigned in nested selectors - You work in a **framework-agnostic setup** - CSS context assignment is independent of the host template engine **Define shared properties with the `*` token:** ```js class UploadBtn extends Symbiote { init$ = { '*files': [] } onUpload(newFile) { this.$['*files'] = [...this.$['*files'], newFile]; } } class FileList extends Symbiote { init$ = { '*files': [] } // same shared prop - first-registered value wins } ``` Both components read and write the same `*files` store. When one updates it, the other reacts automatically - no parent component, no prop drilling, no global store. ### Context name resolution The `ctx` name is resolved in this order (first match wins): 1. `ctx="name"` HTML attribute on the element 2. `--ctx` CSS custom property inherited from ancestors > **IMPORTANT**: In Symbiote 3.x, `*` properties **require** an explicit `ctx` attribute or `--ctx` variable. Without one, no shared context is created, `*` props have no effect, and dev mode will warn (W6). --- ## CSS Data context Symbiote components can initialize their properties from CSS custom property values, enabling CSS-driven configuration: theme tokens, layout parameters, or localized strings - all settable from a stylesheet without touching JavaScript. Use `cssInit$` to explicitly declare CSS-initialized properties with fallback values: ```js class MyWidget extends Symbiote { cssInit$ = { '--columns': 1, '--label': '', } } MyWidget.template = html` {{--label}} `; ``` ```css my-widget { --columns: 3; --label: 'Click me'; } ``` You can also use `--` bindings directly in templates without `cssInit$`: ```js class TestApp extends Symbiote {} TestApp.template = html`

    {{--header}}

    {{--text}}
    `; ``` ```css :root { --header: 'CSS Data'; --text: 'Hello!'; } ``` > CSS custom property values must be valid JSON - use quoted strings (`'text'`), numbers, and `0`/`1` for booleans. > CSS properties are used for **initialization only**. After the component mounts, they act as normal local context properties and no longer track CSS changes. Call `this.updateCssData()` to re-read them after runtime CSS updates. Full details - `updateCssData()`, `dropCssDataCache()`, `ResizeObserver` patterns, and SSR caveats - in the [CSS Data →](./css-data.md) section. --- ## Choosing the right context type | Situation | Use | |-----------|-----| | Component-local reactive state | Local - no token | | Behavior or data passed from an ancestor | Pop-up - `^` | | Sibling components sharing a workflow state | Shared - `*` | | App-wide data, accessed from anywhere in the tree | Named - `/` | | Component configured via CSS / design tokens | CSS Data - `--` | | Reacting to HTML attribute values | Attribute - `@` | | Values derived from other properties | Computed - `+` | --- ## All context types in one component A concise example showing local, attribute, named, shared, and pop-up contexts working together: ```js import Symbiote, { html } from '@symbiotejs/symbiote'; class MyApp extends Symbiote { init$ = { localCtxProp: 'LOCAL', '@attr-test': '', // bound to HTML attribute 'APP/namedProp': 'NAMED', // named context '*sharedProp': 'SHARED', // shared context (requires ctx="..." in HTML) } onUpdate() { let suffix = ' updated'; this.$.localCtxProp += suffix; this.$['APP/namedProp'] += suffix; this.$['*sharedProp'] += suffix; } } MyApp.template = html`
    local: {{localCtxProp}}
    attribute: {{@attr-test}}
    named: {{APP/namedProp}}
    shared: {{*sharedProp}}
    `; MyApp.reg('my-app'); class InnerEl extends Symbiote {} InnerEl.template = html`

    pop-up: {{^localCtxProp}}

    `; InnerEl.reg('inner-el'); ``` ```html ``` --- ## Property token summary | Token | Context Type | Example | |-------|-------------|---------| | _(none)_ | Local | `myProperty` | | `^` | Pop-up | `^parentProp` | | `*` | Shared | `*sharedProp` | | `/` | Named | `APP/myProp` | | `--` | CSS Data | `--my-css-var` | | `@` | Attribute - see [Attributes →](./attributes.md) | `@my-attribute` | | `+` | Computed - see [Properties →](./properties.md#computed-properties) | `+computedProp` | --- # CSS Data Binding Symbiote components can read CSS custom property values into component state. This enables CSS-driven configuration: theme values, layout parameters, localized strings — all settable from CSS without touching JavaScript. ## `cssInit$` Use `cssInit$` to define properties initialized from CSS custom properties: ```js class MyWidget extends Symbiote { cssInit$ = { '--columns': 1, // fallback value if CSS prop not set '--label': '', } } MyWidget.template = html` {{--label}} `; ``` ```css my-widget { --columns: 3; --label: 'Click me'; } ``` CSS values are parsed automatically — quoted strings become strings, numbers become numbers. > Values should be valid JSON, parseable with `JSON.parse()`. Use numbers for boolean flags (`0`/`1`). ## `--` prefix in templates You can bind to CSS custom properties directly in templates: ```js class TestApp extends Symbiote {} TestApp.template = html`

    {{--header}}

    {{--text}}
    `; ``` ```css :root { --header: 'CSS Data'; --text: 'Hello!'; } ``` > CSS custom properties are used for value initialization only. After that, they act like normal local context properties. ## Updating CSS data Call `this.updateCssData()` to re-read CSS custom properties after runtime CSS changes. Call `this.dropCssDataCache()` to clear the cached CSS data. ### Reacting to layout changes CSS data is read once on initialization. To react dynamically — for example, when container queries change custom property values — use `ResizeObserver`: ```js class AdaptiveWidget extends Symbiote { cssInit$ = { '--layout': 'stack', } renderCallback() { new ResizeObserver(() => this.updateCssData()).observe(this); } } ``` ```css adaptive-widget { container-type: inline-size; --layout: 'stack'; } @container (min-width: 600px) { adaptive-widget { --layout: 'grid'; } } ``` > [!WARNING] > **CSS data bindings and SSR.** Computed styles (`getComputedStyle`) are not available during server-side rendering. In `ssrMode` / `isoMode` components, CSS data properties will use their init value (from `cssInit$` or empty string). Enable `devMode` to see warnings for this. ## Why CSS as data? - **Browser-native**: CSS custom properties are a platform feature for passing context down the DOM cascade — no JS library needed. - **Shadow DOM friendly**: CSS custom properties are accessible inside nested shadow roots, unlike other CSS properties. - **Flexible**: Redefine values at any level using `style` or `class` attributes. - **Framework-agnostic**: Works with any external framework, library, or meta-platform. - **CSP-safe**: Unlike inline scripts, CSS is typically allowed by CSP policies — making it ideal for configuration. --- # Dev Mode Enable verbose warnings during development to catch common mistakes early. ## Enabling ```js Symbiote.devMode = true; ``` This also sets `PubSub.devMode = true`. ## Dev messages module All warning and error messages are stored in an optional module. Without it, warnings print short numeric codes like `[Symbiote W5]`. Import the dev messages module once to get full human-readable messages and automatically enable `devMode`: ```js import '@symbiotejs/symbiote/core/devMessages.js'; ``` This single import enables the full dev experience — both verbose messages and dev-only diagnostics. It is typically added in your development entry point and removed (or excluded via tree-shaking) for production builds. ## Warning codes reference | Code | Type | Guard | Description | |------|------|-------|-------------| | W1 | warn | always | PubSub: cannot read/publish/subscribe — property not found | | W2 | warn | devMode | PubSub: type change detected on publish | | W3 | warn | always | PubSub: context already registered | | W4 | warn | always | PubSub: context not found | | W5 | warn | always | Custom template selector not found | | W6 | warn | devMode | `*prop` used without `ctx` attribute or `--ctx` CSS variable | | W7 | warn | devMode | Shared prop already has a value — keeping existing | | W8 | warn | always | Tag already registered with a different class | | W9 | warn | always | CSS data parse error | | W10 | warn | devMode | CSS data binding will not read computed styles during SSR | | W11 | warn | devMode | Binding key not found in `init$` (auto-initialized to `null`) | | W12 | warn | devMode | Text-node binding has no hydration attribute in SSR/ISO mode | | W13 | warn | always | AppRouter message | | W14 | warn | always | History API is not available | | E15 | error | always | `this` used in template interpolation | | W16 | warn | always | Itemize data must be Array or Object | ## Usage Enable `devMode` and load messages in development, omit for production: ```js // development — one import does it all import '@symbiotejs/symbiote/core/devMessages.js'; // or, if you only want devMode without full messages: Symbiote.devMode = true; // production — no overhead, all dev checks are fully gated, // messages module is not loaded ``` --- # Symbiote.js Examples Complete, working examples covering major Symbiote.js features. Each example includes JavaScript, HTML, and CSS where applicable. --- ## 1. Basic Component ```js import Symbiote, { html } from '@symbiotejs/symbiote'; class MyComponent extends Symbiote { count = 0; increment() { this.$.count++; } } MyComponent.template = html`

    {{count}}

    `; MyComponent.reg('my-component'); ``` ```html ``` --- ## 2. Attributes ```js import Symbiote, { html } from '@symbiotejs/symbiote'; class MyComponent extends Symbiote { init$ = { '@attr': 'initial value', prop: 'initial value', checked: true, } } MyComponent.bindAttributes({ 'data-attr': 'prop', }); MyComponent.template = html`

    {{@attr}}

    {{prop}}
    `; MyComponent.reg('my-component'); ``` ```html ``` --- ## 3. Tag Names (Dynamic and Auto-generated) ```js import Symbiote, { html } from '@symbiotejs/symbiote'; class Com1 extends Symbiote {} Com1.template = ``; Com1.reg('my-component'); class Com2 extends Symbiote {} Com2.template = ``; class MyApp extends Symbiote {} MyApp.template = html`

    Tag name is defined explicitly (${Com1.is}), but used dynamically:

    <${Com1.is} />

    Auto-generated tag name: ${Com2.is}

    <${Com2.is} />

    Tag name is used in markup directly:

    `; MyApp.reg('my-app'); ``` ```html ``` --- ## 4. Manual Template Rendering ```js import Symbiote from '@symbiotejs/symbiote'; const HTML = '

    {{text}}

    '; class MyApp extends Symbiote { text = 'Hello world!'; initCallback() { this.render(HTML); } } MyApp.reg('my-app'); ``` ```html ``` --- ## 5. Template Processor (Custom Styling) ```js import Symbiote from '@symbiotejs/symbiote'; import { applyStyles } from '@symbiotejs/symbiote/utils'; const styles = { host: { 'display': 'inline-block', 'padding': '20px', 'border': '1px solid currentColor', }, first_name: { 'color': '#f00', 'font-size': '20px', }, last_name: { 'color': '#00f', 'font-size': '18px', }, }; class StyledComponent extends Symbiote { constructor() { super(); this.templateProcessors.add((fr) => { let cssElArr = [...fr.querySelectorAll('[css]')]; cssElArr.forEach((el) => { let cssName = el.getAttribute('css'); applyStyles(el, styles[cssName]); }); }); applyStyles(this, styles.host); } } class MyApp extends StyledComponent {} MyApp.template = `
    {{firstName}}
    {{lastName}}
    `; MyApp.bindAttributes({ 'first-name': 'firstName', 'last-name': 'lastName', }); MyApp.reg('my-app'); ``` ```html ``` --- ## 6. External Custom Template ```js import Symbiote from '@symbiotejs/symbiote'; class MyCom extends Symbiote { allowCustomTemplate = true; text = 'MY TEXT'; } MyCom.reg('my-com'); ``` ```html ``` --- ## 7. Named Context ```js import Symbiote, { html, PubSub } from '@symbiotejs/symbiote'; PubSub.registerCtx({ name: 'rnd-pro', login: () => { alert('Logged in'); }, }, 'USER'); const appCtx = PubSub.registerCtx({ text: 'Some text', onButtonClick: () => { console.log(appCtx.read('text')); PubSub.getCtx('USER').pub('name', 'NEW NAME ' + Date.now()); } }, 'APP'); class MyDumbComponent extends Symbiote {} MyDumbComponent.template = html`

    User name: {{USER/name}}

    `; MyDumbComponent.reg('my-dumb-component'); ``` ```html ``` --- ## 8. Shared Context Components with the same `ctx` attribute share `*`-prefixed properties. The `--ctx` CSS custom property enables CSS-cascaded context inheritance. ```js import Symbiote from '@symbiotejs/symbiote'; class CtxEl extends Symbiote { init$ = { '*time': 'Click me to show time!', }; renderCallback() { this.onclick = () => { this.$['*time'] = Date.now(); }; } } CtxEl.template = '{{*time}}'; CtxEl.reg('ctx-el'); ``` ```html

    Manual context name (same-level elements):

    CSS-based context (cascading via --ctx):

    ``` ```css ctx-el { display: inline-block; border: 1px solid #00f; padding: 20px; user-select: none; } ``` --- ## 9. All Context Types Demonstrates local, attribute, named (`X/`), shared (`*`), and pop-up (`^`) contexts in one component. ```js import Symbiote, { html } from '@symbiotejs/symbiote'; class MyApp extends Symbiote { init$ = { localCtxProp: 'LOCAL', attributeProp: 'Initial value...', 'X/namedCtxProp': 'NAMED', '*sharedCtxProp': 'SHARED', }; onUpdate() { let updStr = ' updated... '; this.$.localCtxProp += updStr; this.$.attributeProp += updStr; this.$['X/namedCtxProp'] += updStr; this.$['*sharedCtxProp'] += updStr; } } MyApp.template = html`
    {{localCtxProp}}
    {{attributeProp}}
    {{X/namedCtxProp}}
    {{*sharedCtxProp}}
    `; MyApp.bindAttributes({ 'attr-test': 'attributeProp', }); MyApp.reg('my-app'); class InnerEl extends Symbiote {} InnerEl.template = '

    {{^attributeProp}}

    '; InnerEl.reg('inner-el'); ``` ```html ``` ```css my-app { display: block; > * { border: 1px solid green; margin: 10px; padding: 10px; } inner-el { display: inline-block; } } ``` --- ## 10. CSS Data Binding CSS custom properties used as reactive component state. ```js import Symbiote from '@symbiotejs/symbiote'; class MyCom extends Symbiote {} MyCom.template = `

    {{--heading}}

    {{--text}}
    `; MyCom.reg('my-com'); ``` ```html ``` ```css my-com { display: block; padding: 10px; margin: 10px; border: 1px solid green; } .css-data-1 { --heading: 'CSS Data 1'; --text: 'Some text...'; } .css-data-2 { --heading: 'CSS Data 2'; --text: 'Some other text...'; } ``` --- ## 11. Dynamic List Rendering (Itemize with `item-tag`) ```js import Symbiote, { html } from '@symbiotejs/symbiote'; class TableRow extends Symbiote { renderCallback() { this.onclick = () => { this.classList.toggle('selected'); }; } } TableRow.template = ` {{rowNum}} Random number: {{randomNum}} {{date}} `; TableRow.reg('table-row'); class TableApp extends Symbiote { tableData = []; generateTableData() { let data = []; for (let i = 0; i < 1000; i++) { data.push({ rowNum: i + 1, randomNum: Math.random() * 100, date: Date.now(), }); } this.$.tableData = data; } } TableApp.template = html`
    `; TableApp.reg('table-app'); ``` ```html ``` ```css table-row { display: table-row; } table-row.selected { background-color: rgba(255, 0, 200, .3); } td { background-color: rgba(0, 0, 0, .1); padding: 2px; } ``` --- ## 12. Alternative List Rendering (DOM API + animateOut) Direct DOM manipulation for lists, using `ref`, `animateOut`, and CSS enter/exit transitions. ```js import Symbiote, { html } from '@symbiotejs/symbiote'; class ListItem extends Symbiote { onRemove() { Symbiote.animateOut(this); } get checked() { return this.ref.checkbox.checked; } clear() { this.$.text = ''; } renderCallback() { this.ref.edit.focus(); } } ListItem.template = html`
    `; ListItem.reg('list-item'); class MyApp extends Symbiote { get items() { return [...this.ref.list_wrapper.children]; } onAddItem() { this.ref.list_wrapper.appendChild(new ListItem()); } onClearChecked() { this.items.forEach((item) => { if (item.checked) item.clear(); }); } onRemoveChecked() { this.items.forEach((item) => { if (item.checked) Symbiote.animateOut(item); }); } renderCallback() { this.onAddItem(); } } MyApp.template = html`
    `; MyApp.reg('my-app'); ``` ```html ``` ```css list-item { padding: 10px; display: grid; grid-template-columns: min-content auto min-content; border-bottom: 1px solid currentColor; transition: .4s; @starting-style { opacity: 0; transform: translateY(20px); } &[leaving] { opacity: 0; transform: translateX(100px); } } ``` --- ## 13. Nested Lists `processInnerHtml = true` lets you write bindings directly in the component's HTML using the plain `bind=` attribute syntax. ```js import Symbiote from '@symbiotejs/symbiote'; class NestApp extends Symbiote { processInnerHtml = true; data = []; buttonActionName = 'Generate'; onGenerateData() { this.set$({ buttonActionName: 'Update' }); let data = []; for (let i = 0; i < 3; i++) { data.push({ name: i + 1, nestedData: [ { nestedName: 'Nested A', nestedText: Date.now(), nestedData: [ { nestedName: '111', nestedText: Date.now() }, { nestedName: '222', nestedText: Date.now() }, ]}, ], }); } this.$.data = data; } } NestApp.reg('nest-app'); ``` ```html
    {{name}}
    {{nestedName}}
    {{nestedText}}
    {{nestedName}}
    {{nestedText}}
    ``` --- ## 14. CSS-Defined Table Rendering Using CSS `display` values to create table layout from custom elements. ```js import Symbiote from '@symbiotejs/symbiote'; class MyTable extends Symbiote { tableData = []; onSelect(e) { e.target?.closest('table-row')?.classList.toggle('selected'); } } MyTable.template = /*html*/ ` {{rowNumber}} {{date}} `; MyTable.reg('my-table'); ``` ```html ``` ```css table-css { display: table; border-spacing: 2px; table-row { display: table-row; &.selected { background-color: rgba(255, 0, 200, .3); } td-css { display: table-cell; border: 1px solid currentColor; padding: 4px; } } } ``` --- ## 15. Customized Built-in Element as List Item ```js import {} from 'https://cdn.jsdelivr.net/npm/@ungap/custom-elements/+esm'; import Symbiote, { html } from '@symbiotejs/symbiote'; window.customElements.define('option-item', class extends HTMLOptionElement {}, { extends: 'option', }); class MyApp extends Symbiote { init$ = { options: [ { value: '1', textContent: 'Option 1' }, { value: '2', textContent: 'Option 2' }, { value: '3', textContent: 'Option 3' }, ], selectedValue: '1', }; onChange(e) { this.$.selectedValue = e.target.value; } } MyApp.template = html`

    Selected value: {{selectedValue}}

    `; MyApp.reg('my-app'); ``` ```html ``` --- ## 16. SSR Markup Hydration `ssrMode = true` skips template injection and attaches bindings to server-rendered HTML. The component connects to existing DOM without re-rendering. ```js import Symbiote from '@symbiotejs/symbiote'; class MyApp extends Symbiote { ssrMode = true; heading = 'Some heading after hydration'; text = 'Some text after hydration...'; onUpdate() { this.notify('heading'); this.notify('text'); } } MyApp.reg('my-app'); ``` ```html

    Hello world!

    Some initial text from server...
    ``` --- ## 17. SSR CSS Variables Binding CSS custom properties read reactively at runtime, with SSR hydration mode. ```js import Symbiote from '@symbiotejs/symbiote'; class MyCom extends Symbiote { ssrMode = true; onUpdate() { this.$['--text'] = `Updated text... (${Date.now()})`; } } MyCom.reg('my-com'); ``` ```html
    {{--text}}

    {{--heading}}

    {{--text}}

    {{--heading}}

    {{--local-custom-data}}

    {{--text}}
    ``` ```css :root { --heading: 'Root CSS heading'; --text: 'Root CSS text...'; } [container] { --heading: 'Container heading'; --text: 'Container text...'; } .my-class { --heading: 'CSS class heading'; --text: 'CSS class text...'; --local-custom-data: 'Some custom data...'; } my-com { display: block; padding: 20px; border: 1px solid blue; margin-bottom: 10px; } ``` --- ## 18. Localization via PubSub Using a named PubSub context as a reactive l10n store. ```js import Symbiote, { html, PubSub } from '@symbiotejs/symbiote'; const lMap = { EN: { users: 'Users', comments: 'Comments', likes: 'Likes' }, ES: { users: 'Usuarios', comments: 'Comentarios', likes: 'Gustos' }, RU: { users: 'Пользователи', comments: 'Комментарии', likes: 'Лайки' }, }; const l10nCtx = PubSub.registerCtx(lMap.EN, 'L10N'); class MyApp extends Symbiote { numberOfUsers = 10; numberOfComments = 2; numberOfLikes = 12; onLangSelect(e) { l10nCtx.multiPub(lMap[e.target.value]); } } MyApp.template = html`
    {{L10N/users}} -- {{numberOfUsers}}
    {{L10N/comments}} -- {{numberOfComments}}
    {{L10N/likes}} -- {{numberOfLikes}}
    `; MyApp.reg('my-app'); ``` ```html ``` --- ## 19. Icons via Template Processor Custom template processor that replaces `[icon]` attributes with inline SVG. ```js import Symbiote from '@symbiotejs/symbiote'; const ICON_SET = { star: 'M12,17.27L18.18,21L16.54,13.97L22,9.24L14.81,8.62L12,2L9.19,8.62L2,9.24L7.45,13.97L5.82,21L12,17.27Z', ok: 'M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z', }; function getSvg(iconName) { return ` `.trim(); } class AppSuper extends Symbiote { constructor() { super(); this.templateProcessors.add((fr) => { [...fr.querySelectorAll('[icon]')].forEach((el) => { let icon = document.createElement('icon-el'); icon.innerHTML = getSvg(el.getAttribute('icon')); el.prepend(icon); }); }); } } class MyCom extends AppSuper {} MyCom.template = `

    Header

    `; MyCom.reg('my-com'); ``` ```html ``` --- ## 20. Icons via Computed Attribute Binding `isoMode` component using a computed property (`+path`) to drive an SVG attribute. ```js import Symbiote, { html } from '@symbiotejs/symbiote'; const ICONS = { star: 'M12,17.27L18.18,21L16.54,13.97L22,9.24L14.81,8.62L12,2L9.19,8.62L2,9.24L7.45,13.97L5.82,21L12,17.27Z', ok: 'M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z', }; class IconSvg extends Symbiote { isoMode = true; init$ = { '@name': 'star', '+path': () => ICONS[this.$['@name']], } } IconSvg.template = html` `; IconSvg.reg('icon-svg'); ``` ```html

    Heading

    ``` --- ## 21. CSS-Driven Icons SVG path driven entirely by a CSS custom property (`--path`). Zero JS for icon switching. ```js import Symbiote, { html } from '@symbiotejs/symbiote'; class ICon extends Symbiote {} ICon.template = html` `; ICon.reg('i-con'); ``` ```html ``` ```css i-con { display: inline-flex; height: 60px; width: 60px; --path: "M12,17.27L18.18,21L16.54,13.97L22,9.24L14.81,8.62L12,2L9.19,8.62L2,9.24L7.45,13.97L5.82,21L12,17.27Z"; svg { width: 100%; height: 100%; path { fill: currentColor; d: path(var(--path)); } } &[home] { --path: "M10,20V14H14V20H20V12H24L12,0L0,12H4V20H10Z"; } &[dashboard] { --path: "M3,3H11V11H3V3M13,3H21V11H13V3M13,13H21V21H13V13M3,13H11V21H3V13Z"; } } ``` --- ## 22. Universal Tabs Tab navigation using shared context (`*currentTabName`) with no parent coordinator component. ```js import Symbiote from '@symbiotejs/symbiote'; class SuperTabs extends Symbiote { init$ = { '*currentTabName': 'first', }; renderCallback() { this.tabEls = [...this.querySelectorAll('[tab]')]; this.tabEls.forEach((el) => { let tab = el.getAttribute('tab'); if (el.hasAttribute('current')) { this.$['*currentTabName'] = tab; } el.onclick = () => { this.$['*currentTabName'] = tab; }; }); this.sub('*currentTabName', (val) => { this.tabEls.forEach((el) => { el.toggleAttribute('current', el.getAttribute('tab') === val); }); }); } } SuperTabs.reg('super-tabs'); class SuperTabsView extends Symbiote { renderCallback() { this.tabCtxEls = [...this.querySelectorAll('[tab-ctx]')]; this.sub('*currentTabName', (val) => { this.tabCtxEls.forEach((el) => { el.toggleAttribute('active', el.getAttribute('tab-ctx') === val); }); }); } } SuperTabsView.reg('super-tabs-view'); ``` ```html
    First content
    Second content
    Third content
    ``` ```css super-tabs { display: inline-flex; gap: 2px; [tab][current] { background-color: transparent; pointer-events: none; } } super-tabs-view { display: block; [tab-ctx] { display: none; } [tab-ctx][active] { display: contents; } } ``` --- ## 23. Widget Routing via PubSub Custom router using a PubSub named context and computed properties — no AppRouter required. ```js import Symbiote, { html, PubSub } from '@symbiotejs/symbiote'; const routes = [ { route: 'home', title: 'Home', options: { timestamp: Date.now() } }, { route: 'user', title: 'User', options: { timestamp: Date.now() } }, { route: 'settings', title: 'Settings', options: { timestamp: Date.now() } }, ]; const router = PubSub.registerCtx(routes[0], 'R'); class AppShell extends Symbiote { init$ = { routes: structuredClone(routes), '+optionsJson': { deps: ['R/options'], fn: () => JSON.stringify(this.$['R/options'], undefined, 2), }, '+sectionHtml': { deps: ['R/route'], fn: () => { let sec = this.$['R/route']; return `<${sec}-section>`; }, }, onNav: (e) => { let route = e.target.getAttribute('route'); if (route) { let routeDescriptor = routes.find((desc) => desc.route === route); if (routeDescriptor) { routeDescriptor.options.timestamp = Date.now(); router.multiPub(routeDescriptor); } } }, } } AppShell.template = html`

    Section title: {{R/title}}

    Current route: {{R/route}}

    {{+optionsJson}}
    `; AppShell.reg('app-shell'); ``` ```html ``` --- ## 24. AI-Assisted Smart Textarea SSR hydration mode with `ref`, async handlers, and attribute-driven configuration. ```js import Symbiote from '@symbiotejs/symbiote'; const CFG = { apiUrl: 'https://api.openai.com/v1/chat/completions', apiKey: '', }; const textStyles = [ 'Free informal speech, jokes, memes, emoji, possibly long', 'Casual chat, friendly tone, occasional emoji, short and relaxed', 'Medium formality, soft style, compact', 'Neutral tone, clear and direct, minimal slang', 'Professional tone, polite and respectful, no emoji', 'Strict business language, polite and grammatically correct', 'Highly formal, authoritative, complex vocabulary, long and structured', ]; class SmartTextarea extends Symbiote { ssrMode = true; #sourceText = ''; init$ = { '@model': 'gpt-4o', currentTextStyle: textStyles[3], saveSourceText: () => { this.#sourceText = this.ref.text.value; }, revertChanges: () => { this.ref.text.value = this.#sourceText; }, onTextStyleChange: (e) => { this.$.currentTextStyle = textStyles[e.target.value - 1]; }, askAi: async () => { if (!this.ref.text.value.trim()) { alert('Your text input is empty'); return; } let response = await fetch(CFG.apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${CFG.apiKey}`, }, body: JSON.stringify({ model: this.$['@model'], messages: [ { role: 'system', content: JSON.stringify({ useLanguage: this.ref.lang.value || 'Same as input language', textStyle: this.$.currentTextStyle, }), }, { role: 'assistant', content: 'You are a text writing assistant. Rewrite the input text according to the parameters provided.', }, { role: 'user', content: this.ref.text.value, }, ], temperature: 0.7, }), }); let aiResponse = await response.json(); this.ref.text.value = aiResponse?.choices?.[0]?.message.content || this.ref.text.value; }, } } SmartTextarea.reg('smart-textarea'); ``` ```html ``` ```css smart-textarea { display: inline-flex; flex-flow: column; gap: 10px; width: 500px; textarea { width: 100%; height: 200px; } } ``` --- ## 25. rootStyles (Light DOM Adopted Stylesheets) ```js import Symbiote, { html, css } from '@symbiotejs/symbiote'; class MyCard extends Symbiote { init$ = { '@title': 'Card title', '@text': 'Card text content...', } } MyCard.template = html`

    {{@title}}

    {{@text}}

    `; MyCard.rootStyles = css` my-card { display: block; padding: 20px; border: 1px solid var(--border-color, #ccc); border-radius: 8px; background-color: var(--card-bg, #f9f9f9); margin: 10px; max-width: 300px; & h3 { margin: 0 0 8px; color: var(--heading-color, #333); } & p { margin: 0 0 12px; color: var(--text-color, #666); } & button { background-color: var(--accent, #0057ff); color: #fff; border: none; padding: 6px 14px; border-radius: 4px; cursor: pointer; } } `; MyCard.reg('my-card'); ``` ```html
    ``` --- ## 26. shadowStyles (Shadow DOM Isolation) ```js import Symbiote, { html, css } from '@symbiotejs/symbiote'; class MyWidget extends Symbiote { init$ = { '@label': 'Widget', count: 0, } increment() { this.$.count++; } } MyWidget.template = html`
    {{@label}}
    {{count}}
    `; MyWidget.shadowStyles = css` :host { display: inline-block; border: 2px solid #333; border-radius: 8px; overflow: hidden; font-family: sans-serif; margin: 8px; } .header { background: #333; color: #fff; padding: 6px 12px; font-size: 12px; text-transform: uppercase; } .body { padding: 16px; display: flex; align-items: center; gap: 12px; } .count { font-size: 28px; font-weight: bold; min-width: 2ch; text-align: center; } button { background: #0057ff; color: #fff; border: none; border-radius: 4px; padding: 4px 10px; font-size: 18px; cursor: pointer; &:hover { background: #0040cc; } } `; MyWidget.reg('my-widget'); ``` ```html ``` --- # Flags Flags are settings that enable or disable specific features or behaviors. ## Complete list | Flag | Default | Description | |------|---------|-------------| | `renderShadow` | `false` | Render template into Shadow DOM | | `ssrMode` | `false` | Hydrate server-rendered HTML | | `isoMode` | `false` | Isomorphic mode: hydrate if children exist, render template otherwise | | `isVirtual` | `false` | Replace element with its template fragment | | `allowCustomTemplate` | `false` | Allow `use-template` attribute | | `pauseRender` | `false` | Skip automatic rendering | | `processInnerHtml` | `false` | Process existing inner HTML | | `readyToDestroy` | `true` | Allow cleanup on disconnect | | `allowTemplateInits` | `true` | Auto-add props found in template | | `lazyMode` | `false` | Defer initialization until component enters the viewport | | `mcpToolMode` | `false` | Generate experimental WebMCP tools from bound event handlers | ## renderShadow Enables Shadow DOM mode. When enabled, you can use standard slots and `:host` selectors: ```js class MyComponent extends Symbiote { renderShadow = true; } ``` ## ssrMode Enables the hydration workflow — the component uses its own nested markup as a template (provided by the server as regular HTML). Text and attribute bindings are activated on first update, not at state initialization: ```js class MyComponent extends Symbiote { ssrMode = true; } ``` In 3.x, `ssrMode` supports both light DOM and Declarative Shadow DOM hydration. Template injection is skipped; bindings attach to existing DOM. > **Note**: `ssrMode` is a client-side flag. It is separate from `__SYMBIOTE_SSR` (server-side global). See [SSR](./ssr.md) for details. ## isoMode Isomorphic rendering flag. If the component has children when it connects on the client (server-rendered content), it behaves like `ssrMode = true` — hydrates existing DOM. If the component has **no children**, it renders the template normally: ```js class MyComponent extends Symbiote { isoMode = true; } ``` This is useful for components that may or may not be server-rendered — the same component code works in both scenarios without conditional logic. ## isVirtual The component renders its template only, without the wrapping Custom Element. The Custom Element is used as a placeholder and disappears after initial rendering. Data bindings continue to work in memory: ```js class MyComponent extends Symbiote { isVirtual = true; } ``` ## allowCustomTemplate The component's template is taken from an accessible part of the document by the provided selector: ```js class MyComponent extends Symbiote { allowCustomTemplate = true; } ``` Then use the `use-template` attribute: ```html ``` ## pauseRender Disables the default render stage to allow additional logic before rendering. Call `render()` manually: ```js class MyComponent extends Symbiote { pauseRender = true; initCallback() { fetch('../my-data.json').then((response) => { response.json().then((data) => { this.set$(data); this.render(); }); }); } } ``` ## processInnerHtml Similar to `ssrMode`, but all initiated data is rendered immediately. You can use standard template binding syntax for text nodes: ```js class MyComponent extends Symbiote { processInnerHtml = true; } ``` ```html

    {{someHeading}}

    ``` ## readyToDestroy Controls whether the component is destroyed when removed from DOM. Set to `false` to **disable** destruction: ```js class MyComponent extends Symbiote { readyToDestroy = false; } ``` See also: `destructionDelay` in [Lifecycle](./lifecycle.md). ## allowTemplateInits Controls automated property initialization from template mentions. Disable to require explicit `init$` declarations: ```js class MyComponent extends Symbiote { allowTemplateInits = false; } // The 'heading' property won't be auto-initialized: MyComponent.template = html`

    {{heading}}

    `; ``` ## lazyMode Defers component initialization until it enters the viewport using a global \`IntersectionObserver\`. When the component scrolls out of view, its internal DOM is cleared to save memory, and its dimensions (\`min-height\` and \`min-width\`) are preserved to prevent scrollbar jumping. State updates are preserved while hidden and applied when the component re-enters the viewport. ```js class MyComponent extends Symbiote { lazyMode = true; } ``` This is most commonly used by adding the \`lazy\` attribute to an \`itemize\` container to optimize rendering of massive lists: ```html