@symbiotejs/symbiote 3.8.1 → 3.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/llms-full.txt CHANGED
@@ -4,12 +4,10 @@
4
4
 
5
5
  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.
6
6
 
7
- Symbiote.js gives you the convenience of a modern framework while staying close to the native platform - HTML, CSS, and DOM APIs. Components are custom elements that work everywhere: in any framework, in plain HTML, in a micro-frontend architecture. And with **isomorphic mode**, the same component code works on the server and the client - server-rendered pages hydrate automatically, no diffing, no mismatch errors.
8
-
9
7
  Here are the three most important differences between Symbiote.js and other frameworks:
10
- 1. Natural DOM Extension Philosophy - designed to extend platform, not to replace it
11
- 2. Runtime-Agnostic HTML Templates - outstanding flexibility for rendering strategies and further customization
12
- 3. Powerful App-wide State Management - combine data contexts without bloated boilerplate or external tools
8
+ 1. **Natural DOM Extension Philosophy** - designed to extend platform APIs, not to replace them
9
+ 2. **Runtime-Agnostic HTML Templates** - outstanding flexibility for rendering strategies and further customization
10
+ 3. **Powerful App-wide State Management** - combine data contexts without bloated boilerplate or external tools
13
11
 
14
12
  ## What's new in v3.x?
15
13
 
@@ -23,7 +21,6 @@ Here are the three most important differences between Symbiote.js and other fram
23
21
  - **DSD hydration** - `ssrMode` supports both light DOM and Declarative Shadow DOM.
24
22
  - **Class property fallback** - binding keys not in `init$` fall back to own class properties/methods.
25
23
  - **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.
26
- - And [more](https://github.com/symbiotejs/symbiote.js/blob/webmcp/CHANGELOG.md).
27
24
 
28
25
  ## Quick start
29
26
 
@@ -61,57 +58,6 @@ npm i @symbiotejs/symbiote
61
58
  import Symbiote, { html, css } from '@symbiotejs/symbiote';
62
59
  ```
63
60
 
64
- ## Isomorphic Web Components
65
-
66
- One component. Server-rendered or client-rendered - automatically. Set `isoMode = true` and the component figures it out: if server-rendered content exists, it hydrates; otherwise it renders from template. No conditional logic, no separate server/client versions:
67
- ```js
68
- class MyComponent extends Symbiote {
69
- isoMode = true;
70
- count = 0;
71
- increment() {
72
- this.$.count++;
73
- }
74
- }
75
-
76
- MyComponent.template = html`
77
- <h2 ${{textContent: 'count'}}></h2>
78
- <button ${{onclick: 'increment'}}>Click me!</button>
79
- `;
80
- MyComponent.reg('my-component');
81
- ```
82
-
83
- This exact code runs **everywhere** - SSR on the server, hydration on the client, or pure client rendering. No framework split, no `'use client'` directives, no hydration mismatch errors.
84
-
85
- ### SSR - one class, zero config
86
-
87
- Server rendering doesn't need a virtual DOM, a reconciler, or framework-specific packages:
88
-
89
- ```js
90
- import { SSR } from '@symbiotejs/symbiote/node/SSR.js';
91
-
92
- await SSR.init(); // patches globals with linkedom
93
- await import('./my-app.js'); // components register normally
94
-
95
- let html = await SSR.processHtml('<my-app></my-app>');
96
- SSR.destroy();
97
- ```
98
-
99
- For large pages, stream HTML chunks with `SSR.renderToStream()` for faster TTFB. See [SSR docs](./docs/ssr.md) and [server setup recipes](./docs/ssr-server.md).
100
-
101
- ### How it compares
102
-
103
- | | **Symbiote.js** | **Next.js (React)** | **Lit** (`@lit-labs/ssr`) |
104
- |--|----------------|---------------------|----|
105
- | **Isomorphic code** | Same code, `isoMode` auto-detects | Server Components vs Client Components split | Same code, but load-order constraints |
106
- | **Hydration** | Binding-based - attaches to existing DOM, no diffing | `hydrateRoot()` - must produce identical output or errors | Requires `ssr-client` + hydrate support module |
107
- | **Packages** | 1 module + `linkedom` peer dep | Full framework buy-in | 3 packages: `ssr`, `ssr-client`, `ssr-dom-shim` |
108
- | **Streaming** | `renderToStream()` async generator | `renderToPipeableStream()` | Iterable `RenderResult` |
109
- | **Mismatch handling** | Not needed - bindings attach to whatever DOM exists | Hard errors if server/client output differs | N/A |
110
- | **Template output** | Clean HTML with `bind=` attributes | HTML with framework markers | HTML with `<!--lit-part-->` comment markers |
111
- | **Lock-in** | None - standard Web Components | Full framework commitment | Lit-specific, but Web Components |
112
-
113
- **Key insight:** There are no hydration mismatches because there's no diffing. The server produces HTML with binding attributes. The client reads those attributes and adds reactivity. That's it.
114
-
115
61
  ## Core concepts
116
62
 
117
63
  ### Reactive state
@@ -140,7 +86,7 @@ This makes it easy to control Symbiote-based widgets and microfrontends from any
140
86
 
141
87
  ### Templates
142
88
 
143
- Templates are plain HTML strings - context-free, easy to test, easy to move between files:
89
+ Templates are plain HTML strings - runtime-agnostic, easy to test, easy to move between files:
144
90
 
145
91
  ```js
146
92
  // Separate file: my-component.template.js
@@ -158,34 +104,24 @@ The `html` function supports two interpolation modes:
158
104
 
159
105
  ### Itemize (dynamic reactive lists)
160
106
 
161
- Render lists from data arrays with efficient updates:
107
+ Render lists from data arrays or objects with efficient updates:
162
108
  ```js
163
109
  class TaskList extends Symbiote {
164
110
  tasks = [
165
111
  { name: 'Buy groceries' },
166
112
  { name: 'Write docs' },
167
113
  ];
168
- init$ = {
169
- // Needs to be defined in init$ for pop-up binding to work
170
- onItemClick: () => {
171
- console.log('clicked!');
172
- },
173
- }
174
114
  }
175
115
 
176
116
  TaskList.template = html`
177
- <div itemize="tasks">
117
+ <ul itemize="tasks">
178
118
  <template>
179
- <div ${{onclick: '^onItemClick'}}>{{name}}</div>
119
+ <li>{{name}}</li>
180
120
  </template>
181
- </div>
121
+ </ul>
182
122
  `;
183
123
  ```
184
124
 
185
- Items have their own state scope. Use the **`^` prefix** to reach higher-level component properties and handlers - `'^onItemClick'` binds to the parent's `onItemClick`, not the item's. Properties referenced via `^` must be defined in the parent's `init$`.
186
-
187
- > **Performance Tip:** For massive lists, add the `lazy` attribute to the container (`<div itemize="tasks" lazy>`). It defers component initialization until they enter the viewport and cleans them up when they leave, heavily optimizing memory and rendering performance.
188
-
189
125
  ### Pop-up binding (`^`)
190
126
 
191
127
  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$()`):
@@ -198,14 +134,11 @@ The `^` prefix works in any nested component template - it walks up the DOM tree
198
134
  <button ${{onclick: '^parentHandler'}}>Click</button>
199
135
  ```
200
136
 
201
- > **Note:** Class property fallbacks are not checked by the `^` walk - the parent must define the property in `init$`.
202
-
203
137
  ### Named data contexts
204
138
 
205
139
  Share state across components without prop drilling:
206
-
207
140
  ```js
208
- import { PubSub } from '@symbiotejs/symbiote';
141
+ import { PubSub, html } from '@symbiotejs/symbiote';
209
142
 
210
143
  PubSub.registerCtx({
211
144
  user: 'Alex',
@@ -214,6 +147,9 @@ PubSub.registerCtx({
214
147
 
215
148
  // Any component can read/write:
216
149
  this.$['APP/user'] = 'New name';
150
+
151
+ // Any template can use property directly:
152
+ let template = html`<h2>{{APP/user}}</h2>`;
217
153
  ```
218
154
 
219
155
  ### Shared context (`*`)
@@ -246,11 +182,10 @@ class StatusBar extends Symbiote {
246
182
 
247
183
  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.
248
184
 
249
- The context name can also be inherited via CSS custom property `--ctx`, enabling layout-driven grouping.
250
-
251
- ### Routing (optional module)
185
+ ### Application routing
252
186
 
253
187
  ```js
188
+ // Import optional module:
254
189
  import { AppRouter } from '@symbiotejs/symbiote/core/AppRouter.js';
255
190
 
256
191
  AppRouter.initRoutingCtx('R', {
@@ -260,23 +195,7 @@ AppRouter.initRoutingCtx('R', {
260
195
  });
261
196
  ```
262
197
 
263
- ### Exit animations
264
-
265
- CSS-driven transitions with zero JS animation code:
266
-
267
- ```css
268
- task-item {
269
- opacity: 1;
270
- transition: opacity 0.3s;
271
-
272
- @starting-style { opacity: 0; } /* enter */
273
- &[leaving] { opacity: 0; } /* exit */
274
- }
275
- ```
276
-
277
- `animateOut(el)` sets `[leaving]`, waits for `transitionend`, then removes. Itemize uses this automatically.
278
-
279
- ### Styling
198
+ ### CSS Styling
280
199
 
281
200
  Shadow DOM is **optional** in Symbiote - use it when you need isolation, skip it when you don't. This gives full flexibility:
282
201
 
@@ -293,6 +212,8 @@ MyComponent.rootStyles = css`
293
212
  `;
294
213
  ```
295
214
 
215
+ This style will be applied to nearest upper shadow root, if exists and to common document if not.
216
+
296
217
  **Shadow DOM** - opt-in isolation when needed:
297
218
 
298
219
  ```js
@@ -306,9 +227,9 @@ Isolated.shadowStyles = css`
306
227
 
307
228
  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.
308
229
 
309
- ### CSS Data Binding
230
+ ### CSS Data
310
231
 
311
- Components can read CSS custom properties as reactive state via `cssInit$`:
232
+ Components can read CSS custom property values to initiate reactive state:
312
233
 
313
234
  ```css
314
235
  my-widget {
@@ -324,8 +245,6 @@ MyWidget.template = html`
324
245
  `;
325
246
  ```
326
247
 
327
- CSS values are parsed automatically - quoted strings become strings, numbers become numbers. Call `this.updateCssData()` to re-read after runtime CSS changes. This enables CSS-driven configuration: theme values, layout parameters, or localized strings - all settable from CSS without touching JS.
328
-
329
248
  ## Best for
330
249
 
331
250
  - **Complex widgets** embedded in any host application
@@ -336,19 +255,13 @@ CSS values are parsed automatically - quoted strings become strings, numbers bec
336
255
  - **Framework-agnostic solutions** - one codebase, any context
337
256
  - **Modern AI-first web** - expose the application state to WebMCP tools automatically
338
257
 
339
- ## Browser support
340
-
341
- All modern browsers: Chrome, Firefox, Safari, Edge, Opera.
342
-
343
258
  ## Docs & Examples
344
259
 
345
260
  - [Documentation](https://github.com/symbiotejs/symbiote.js/blob/main/docs/README.md)
346
- - [Lit vs Symbiote.js](https://github.com/symbiotejs/symbiote.js/blob/main/docs/lit-vs-symbiote.md) - Side-by-side comparison
347
261
  - [Live Examples](https://rnd-pro.com/symbiote/3x/examples/) - Interactive Code Playground
348
262
  - [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
349
263
  - [AI / llms.txt](https://rnd-pro.com/symbiote/llms.txt) — index for AI tools
350
264
  - [Full docs (single file)](https://rnd-pro.com/symbiote/llms-full.txt) — complete merged reference for AI context
351
- - [Changelog](https://github.com/symbiotejs/symbiote.js/blob/main/CHANGELOG.md)
352
265
 
353
266
  ## Related articles
354
267
 
@@ -405,7 +318,7 @@ my-item {
405
318
 
406
319
  ## Itemize integration
407
320
 
408
- Both itemize processors use `animateOut` automatically for item removal. Items with CSS `transition` + `[leaving]` styles will animate out before being removed from the DOM:
321
+ Itemize processors use `animateOut` automatically for item removal. Items with CSS `transition` + `[leaving]` styles will animate out before being removed from the DOM:
409
322
  ```css
410
323
  user-card {
411
324
  opacity: 1;
@@ -449,7 +362,7 @@ class MyComponent extends Symbiote {}
449
362
  MyComponent.template = html`<h1>{{@attribute-name}}</h1>`;
450
363
  ```
451
364
 
452
- Then use it as an attribute in markup:
365
+ Then use it as an component's attribute in markup:
453
366
  ```html
454
367
  <my-component attribute-name="attribute value"></my-component>
455
368
  ```
@@ -473,7 +386,7 @@ MyComponent.observedAttributes = [
473
386
 
474
387
  ## `bindAttributes()` static method
475
388
 
476
- Bind attributes to property values directly:
389
+ Reflect attribute values to property values:
477
390
  ```js
478
391
  class MyComponent extends Symbiote {
479
392
 
@@ -754,7 +667,69 @@ Named external contexts (`CTX/prop`) are different: they are registered globally
754
667
 
755
668
  ---
756
669
 
757
- ## 10. Treating `init$` as a plain object
670
+ ## 10. Using `{{prop}}` binding syntax inside tag definitions
671
+
672
+ `{{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.
673
+
674
+ ```html
675
+ <!-- WRONG — {{}} syntax does not work inside tag definitions -->
676
+ <div class="{{APP/theme}}">...</div>
677
+ <img src="{{imageUrl}}">
678
+ <button onclick="{{onAction}}">Go</button>
679
+
680
+ <!-- CORRECT — use ${{}} binding object inside the tag -->
681
+ <div ${{'@class': 'APP/theme'}}>...</div>
682
+ <img ${{'@src': 'imageUrl'}}>
683
+ <button ${{onclick: 'onAction'}}>Go</button>
684
+ <div ${{'style.color': 'colorProp'}}>...</div>
685
+
686
+ <!-- CORRECT — or use the bind attribute string syntax -->
687
+ <div bind="@class: APP/theme">...</div>
688
+ <img bind="@src: imageUrl">
689
+ <button bind="onclick: onAction">Go</button>
690
+
691
+ <!-- CORRECT — {{}} works fine in text node content -->
692
+ <h1>Hello, {{userName}}!</h1>
693
+ <p>Theme: {{APP/theme}}</p>
694
+ ```
695
+
696
+ ---
697
+
698
+ ## 11. Expecting two-way data binding
699
+
700
+ 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.
701
+
702
+ ```js
703
+ // WRONG — expecting the binding to also update state on user input
704
+ MyComponent.template = html`
705
+ <input ${{value: 'query'}}>
706
+ `;
707
+
708
+ // CORRECT — handle the input event and write back to state
709
+ class MyComponent extends Symbiote {
710
+ init$ = { query: '' }
711
+ }
712
+
713
+ MyComponent.template = html`
714
+ <input
715
+ ${{value: 'query', oninput: 'onInput'}}
716
+ >
717
+ `;
718
+ ```
719
+ ```js
720
+ class MyComponent extends Symbiote {
721
+ init$ = {
722
+ query: '',
723
+ onInput: (e) => { this.$.query = e.target.value; },
724
+ }
725
+ }
726
+ ```
727
+
728
+ This is intentional — explicit event handling keeps data flow predictable and avoids the hidden side effects that two-way binding can introduce.
729
+
730
+ ---
731
+
732
+ ## 12. Treating `init$` as a plain object
758
733
 
759
734
  `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.
760
735
 
@@ -780,39 +755,46 @@ class MyComponent extends Symbiote {
780
755
 
781
756
  # Context
782
757
 
783
- Usage context is one of the central things in Symbiote.js. Every Symbiote component is able to analyze its environment, read external settings from its actual position in DOM, and provide data to related components. Symbiote.js utilizes the DOM structure as the basic entity for data flow management and interconnection.
758
+ 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.
759
+
760
+ There are seven context types, each addressed by a token prefix in property keys:
784
761
 
785
- Component context is a sum of all accessible data sources. Each source represents its own type of interaction. Let's have a look at them.
762
+ | Token | Context Type | Scope |
763
+ |-------|-------------|-------|
764
+ | _(none)_ | [Local](#local-context) | Component instance |
765
+ | `^` | [Pop-up](#pop-up-context) | Nearest ancestor that owns the property |
766
+ | `*` | [Shared](#shared-context) | All components sharing a `ctx` attribute |
767
+ | `name/` | [Named](#named-context) | Any component, anywhere |
768
+ | `--` | [CSS Data](#css-data-context) | Inherited from the CSS cascade |
769
+ | `@` | [Attribute](./attributes.md) | HTML attribute on the element |
770
+ | `+` | [Computed](./properties.md#computed-properties) | Derived, auto-recalculated value |
771
+
772
+ ---
786
773
 
787
774
  ## Local context
788
775
 
789
- Local context properties work the same way as component state in most other libraries:
776
+ 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.
777
+
778
+ Define properties in `init$`:
790
779
  ```js
791
780
  class MyComponent extends Symbiote {
792
-
793
781
  init$ = {
794
782
  myProperty: 'some value',
795
783
  }
796
-
797
784
  }
798
785
  ```
799
786
 
800
- Use the `$` proxy to read and write values:
787
+ Use the `$` proxy to read and write values at runtime:
801
788
  ```js
802
789
  class MyComponent extends Symbiote {
803
-
804
790
  init$ = {
805
791
  myProperty: 'some value',
806
792
  }
807
793
 
808
794
  renderCallback() {
809
- // Read:
810
795
  console.log(this.$.myProperty); // > 'some value'
811
-
812
- // Write:
813
796
  this.$.myProperty = 'new value';
814
797
  }
815
-
816
798
  }
817
799
 
818
800
  MyComponent.template = html`
@@ -820,97 +802,143 @@ MyComponent.template = html`
820
802
  `;
821
803
  ```
822
804
 
805
+ 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:
806
+ ```js
807
+ class MyComponent extends Symbiote {
808
+ count = 0;
809
+ label = 'Hello';
810
+ }
811
+ ```
812
+
813
+ > Use `init$` when you need shared (`*`), computed (`+`), or attribute (`@`) props in the same declaration - those tokens are only recognized inside `init$`.
814
+
815
+ See [Properties →](./properties.md) for the full property API: `add$()`, `sub()`, `set$()`, computed props, and more.
816
+
817
+ ---
818
+
823
819
  ## Named context
824
820
 
825
- Named context is an external abstract data source accessed by its name. It can contain any application data or be used for a dedicated purpose.
821
+ 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.
826
822
 
827
- Example — using named context as a localization tool:
823
+ Use named context when data must be shared across unrelated parts of the application.
824
+
825
+ **Creating a named context:**
826
+ ```js
827
+ import { PubSub } from '@symbiotejs/symbiote';
828
+
829
+ let appCtx = PubSub.registerCtx({
830
+ theme: 'light',
831
+ user: null,
832
+ }, 'APP');
833
+ ```
834
+
835
+ **Accessing it from a component:**
836
+ ```js
837
+ class MyComponent extends Symbiote {
838
+ init$ = {
839
+ 'APP/theme': 'light', // optional local fallback - used if the named context hasn't published yet
840
+ }
841
+
842
+ renderCallback() {
843
+ console.log(this.$['APP/theme']); // read
844
+ this.$['APP/theme'] = 'dark'; // write - updates the named context for all subscribers
845
+ }
846
+ }
847
+
848
+ MyComponent.template = html`
849
+ <div ${{'@class': 'APP/theme'}}>...</div>
850
+ `;
851
+ ```
852
+
853
+ **Example - localization:**
828
854
  ```js
829
855
  import Symbiote, { html, PubSub } from '@symbiotejs/symbiote';
830
856
 
831
- // Create localization map for English:
832
- let EN = {
857
+ let l10nCtx = PubSub.registerCtx({
833
858
  users: 'Users',
834
859
  comments: 'Comments',
835
860
  likes: 'Likes',
836
- };
837
-
838
- // Create localization context:
839
- let l10nCtx = PubSub.registerCtx(EN, 'L10N');
840
-
841
- // Use localized strings in templates:
842
- class MyComponent extends Symbiote { ... }
861
+ }, 'L10N');
843
862
 
844
863
  MyComponent.template = html`
845
864
  <div>{{L10N/users}} - {{numberOfUsers}}</div>
846
865
  <div>{{L10N/comments}} - {{numberOfComments}}</div>
847
866
  <div>{{L10N/likes}} - {{numberOfLikes}}</div>
848
867
  `;
849
- ```
850
868
 
851
- Use `/` token with the context key to access properties: `L10N/users`.
852
-
853
- Switching language:
854
- ```js
855
- let ES = {
869
+ // Switch language at any time - all subscribed components update instantly:
870
+ l10nCtx.multiPub({
856
871
  users: 'Usuarios',
857
872
  comments: 'Comentarios',
858
873
  likes: 'Gustos',
859
- };
860
-
861
- l10nCtx.multiPub(ES);
874
+ });
862
875
  ```
863
876
 
864
- Read and modify named context properties with the `$` proxy:
877
+ You can also read and modify named context directly from any component using the `$` proxy:
865
878
  ```js
866
879
  class MyComponent extends Symbiote {
867
880
  renderCallback() {
868
- // Read:
869
881
  console.log(this.$['L10N/users']);
870
-
871
- // Modify:
872
882
  this.$['L10N/users'] = 'ユーザー';
873
883
  }
874
884
  }
875
885
  ```
876
886
 
877
- More information about `PubSub` in the [PubSub section](./pubsub.md).
887
+ More information about `PubSub` in the [PubSub ](./pubsub.md) section.
888
+
889
+ ---
878
890
 
879
891
  ## Pop-up context
880
892
 
881
- Pop-up binding helps control component interactions based on their DOM position and hierarchy. It works similarly to the CSS cascade model.
893
+ 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.
894
+
895
+ Use the `^` token to reference a pop-up property - in both text bindings and event handlers:
896
+ ```html
897
+ <!-- Text binding to ancestor property: -->
898
+ <div>{{^parentTitle}}</div>
899
+
900
+ <!-- Handler binding to ancestor method: -->
901
+ <button ${{onclick: '^onButtonClicked'}}>Click me!</button>
902
+ ```
882
903
 
883
- Use the `^` token to reference a higher-level property:
884
904
  ```js
885
- class MyComponent extends Symbiote {}
905
+ class MyButton extends Symbiote {}
886
906
 
887
- MyComponent.template = html`
907
+ MyButton.template = html`
908
+ <span>{{^parentTitle}}</span>
888
909
  <button ${{onclick: '^onButtonClicked'}}>Click me!</button>
889
910
  `;
890
911
  ```
891
- Symbiote walks up the DOM tree until it finds the component with `onButtonClicked` registered in its data context.
912
+
913
+ Symbiote walks up the DOM tree until it finds a component with the requested property registered in its context:
914
+ ```js
915
+ class MyEditor extends Symbiote {
916
+ init$ = {
917
+ onButtonClicked: () => console.log('clicked'),
918
+ editorTitle: 'My Editor',
919
+ }
920
+ }
921
+ ```
892
922
 
893
923
  > [!IMPORTANT]
894
- > The `^` lookup only checks the parent's **data context** (properties registered via `init$` or `add$()`). Class property fallbacks are **not** resolved during this walk. Always define `^`-targeted properties in the parent's `init$`:
924
+ > 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$`:
895
925
  > ```js
896
926
  > class ParentComponent extends Symbiote {
897
927
  > init$ = {
898
- > onButtonClicked: () => { console.log('clicked'); },
928
+ > onButtonClicked: () => console.log('clicked'),
899
929
  > parentTitle: 'Hello',
900
930
  > }
901
931
  > }
902
932
  > ```
903
933
 
904
- This is useful for composition, customization, and responsibility splitting:
934
+ Pop-up context is useful for composition - the same child component adapts to whichever ancestor provides the expected behavior:
905
935
  ```js
906
936
  html`
907
937
  <my-text-editor>
908
938
  <complete-toolbar></complete-toolbar>
909
939
  </my-text-editor>
910
940
  `;
911
- ```
912
- Or:
913
- ```js
941
+ // or:
914
942
  html`
915
943
  <my-text-editor>
916
944
  <simplified-toolbar></simplified-toolbar>
@@ -919,19 +947,21 @@ html`
919
947
  `;
920
948
  ```
921
949
 
922
- > Like in CSS, pop-up properties have no collision guard use additional prefixes in uncontrolled environments.
950
+ > 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.
951
+
952
+ ---
923
953
 
924
954
  ## Shared context
925
955
 
926
- Shared context works similarly to native HTML radio inputs set the `name` attribute and different inputs connect into one workflow.
956
+ Shared context is inspired by native HTML `name` attributes - the same way `<input name="group">` 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.
927
957
 
928
- In 3.x, an explicit context name is **required** for shared properties. Set it using the `ctx` HTML attribute:
958
+ **Assign a context name via the `ctx` HTML attribute:**
929
959
  ```html
930
960
  <upload-btn ctx="gallery"></upload-btn>
931
961
  <file-list ctx="gallery"></file-list>
932
962
  ```
933
963
 
934
- Or using the `--ctx` CSS custom property:
964
+ **Or via the `--ctx` CSS custom property**, which cascades like any CSS variable:
935
965
  ```css
936
966
  .gallery-section {
937
967
  --ctx: gallery;
@@ -945,49 +975,64 @@ Or using the `--ctx` CSS custom property:
945
975
  ```
946
976
 
947
977
  The CSS approach is useful when:
948
- - You want **layout-driven grouping** components inherit the context from their visual container rather than repeating the attribute on each one
949
- - You need to **reassign context at different DOM levels** just like any CSS custom property, `--ctx` cascades and can be overridden in nested selectors
950
- - You work in a **framework-agnostic setup** CSS can be managed separately from markup, so context assignment doesn't depend on template engine or host framework
978
+ - You want **layout-driven grouping** - components inherit context from their visual container rather than repeating the attribute on each one
979
+ - You need to **override context at different DOM levels** - just like any CSS custom property, `--ctx` cascades and can be reassigned in nested selectors
980
+ - You work in a **framework-agnostic setup** - CSS context assignment is independent of the host template engine
951
981
 
952
- Then use the `*` token to define shared properties:
982
+ **Define shared properties with the `*` token:**
953
983
  ```js
954
984
  class UploadBtn extends Symbiote {
955
985
  init$ = { '*files': [] }
956
986
 
957
- onUpload() {
987
+ onUpload(newFile) {
958
988
  this.$['*files'] = [...this.$['*files'], newFile];
959
989
  }
960
990
  }
961
991
 
962
992
  class FileList extends Symbiote {
963
- init$ = { '*files': [] } // same shared prop first-registered value wins
993
+ init$ = { '*files': [] } // same shared prop - first-registered value wins
964
994
  }
965
995
  ```
966
996
 
967
- Both components access the same `*files` state no parent component, no prop drilling, no global store.
997
+ 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.
968
998
 
969
999
  ### Context name resolution
970
1000
 
971
- The context name is resolved in this order (first match wins):
1001
+ The `ctx` name is resolved in this order (first match wins):
1002
+
1003
+ 1. `ctx="name"` HTML attribute on the element
1004
+ 2. `--ctx` CSS custom property inherited from ancestors
972
1005
 
973
- 1. `ctx="name"` HTML attribute
974
- 2. `--ctx` CSS custom property (inherited from ancestors)
1006
+ > **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).
975
1007
 
976
- > **IMPORTANT**: In 3.x, `*` properties **require** an explicit `ctx` attribute or `--ctx` CSS variable. Without one, the shared context is not created, `*` props have no effect, and dev mode will warn about it.
1008
+ ---
977
1009
 
978
1010
  ## CSS Data context
979
1011
 
980
- Symbiote components can initiate their properties from CSS custom property values:
981
- ```css
982
- :root {
983
- --header: 'CSS Data';
984
- --text: 'Hello!';
1012
+ 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.
1013
+
1014
+ Use `cssInit$` to explicitly declare CSS-initialized properties with fallback values:
1015
+ ```js
1016
+ class MyWidget extends Symbiote {
1017
+ cssInit$ = {
1018
+ '--columns': 1,
1019
+ '--label': '',
1020
+ }
985
1021
  }
1022
+
1023
+ MyWidget.template = html`
1024
+ <span>{{--label}}</span>
1025
+ `;
986
1026
  ```
987
1027
 
988
- > CSS custom property values should be valid JSON values, parseable with `JSON.parse()`. Use numbers for boolean flags (`0`/`1`).
1028
+ ```css
1029
+ my-widget {
1030
+ --columns: 3;
1031
+ --label: 'Click me';
1032
+ }
1033
+ ```
989
1034
 
990
- Use CSS values in templates directly:
1035
+ You can also use `--` bindings directly in templates without `cssInit$`:
991
1036
  ```js
992
1037
  class TestApp extends Symbiote {}
993
1038
 
@@ -996,10 +1041,79 @@ TestApp.template = html`
996
1041
  <div>{{--text}}</div>
997
1042
  `;
998
1043
  ```
1044
+ ```css
1045
+ :root {
1046
+ --header: 'CSS Data';
1047
+ --text: 'Hello!';
1048
+ }
1049
+ ```
999
1050
 
1000
- > CSS custom properties are used for value initialization only. After that, they act like normal local context properties.
1051
+ > CSS custom property values must be valid JSON - use quoted strings (`'text'`), numbers, and `0`/`1` for booleans.
1052
+
1053
+ > 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.
1054
+
1055
+ Full details - `updateCssData()`, `dropCssDataCache()`, `ResizeObserver` patterns, and SSR caveats - in the [CSS Data →](./css-data.md) section.
1056
+
1057
+ ---
1058
+
1059
+ ## Choosing the right context type
1060
+
1061
+ | Situation | Use |
1062
+ |-----------|-----|
1063
+ | Component-local reactive state | Local - no token |
1064
+ | Behavior or data passed from an ancestor | Pop-up - `^` |
1065
+ | Sibling components sharing a workflow state | Shared - `*` |
1066
+ | App-wide data, accessed from anywhere in the tree | Named - `/` |
1067
+ | Component configured via CSS / design tokens | CSS Data - `--` |
1068
+ | Reacting to HTML attribute values | Attribute - `@` |
1069
+ | Values derived from other properties | Computed - `+` |
1070
+
1071
+ ---
1001
1072
 
1002
- More details in the [CSS Data](./css-data.md) section.
1073
+ ## All context types in one component
1074
+
1075
+ A concise example showing local, attribute, named, shared, and pop-up contexts working together:
1076
+
1077
+ ```js
1078
+ import Symbiote, { html } from '@symbiotejs/symbiote';
1079
+
1080
+ class MyApp extends Symbiote {
1081
+ init$ = {
1082
+ localCtxProp: 'LOCAL',
1083
+ '@attr-test': '', // bound to HTML attribute
1084
+ 'APP/namedProp': 'NAMED', // named context
1085
+ '*sharedProp': 'SHARED', // shared context (requires ctx="..." in HTML)
1086
+ }
1087
+
1088
+ onUpdate() {
1089
+ let suffix = ' updated';
1090
+ this.$.localCtxProp += suffix;
1091
+ this.$['APP/namedProp'] += suffix;
1092
+ this.$['*sharedProp'] += suffix;
1093
+ }
1094
+ }
1095
+
1096
+ MyApp.template = html`
1097
+ <div>local: {{localCtxProp}}</div>
1098
+ <div>attribute: {{@attr-test}}</div>
1099
+ <div>named: {{APP/namedProp}}</div>
1100
+ <div>shared: {{*sharedProp}}</div>
1101
+ <button ${{onclick: 'onUpdate'}}>Update</button>
1102
+ <inner-el></inner-el> <!-- reads ^localCtxProp via pop-up -->
1103
+ `;
1104
+
1105
+ MyApp.reg('my-app');
1106
+
1107
+ class InnerEl extends Symbiote {}
1108
+ InnerEl.template = html`<h2>pop-up: {{^localCtxProp}}</h2>`;
1109
+ InnerEl.reg('inner-el');
1110
+ ```
1111
+
1112
+ ```html
1113
+ <my-app attr-test="HTML value" ctx="my-ctx"></my-app>
1114
+ ```
1115
+
1116
+ ---
1003
1117
 
1004
1118
  ## Property token summary
1005
1119
 
@@ -1010,8 +1124,8 @@ More details in the [CSS Data](./css-data.md) section.
1010
1124
  | `*` | Shared | `*sharedProp` |
1011
1125
  | `/` | Named | `APP/myProp` |
1012
1126
  | `--` | CSS Data | `--my-css-var` |
1013
- | `@` | Attribute | `@my-attribute` |
1014
- | `+` | Computed | `+computedProp` |
1127
+ | `@` | Attribute - see [Attributes →](./attributes.md) | `@my-attribute` |
1128
+ | `+` | Computed - see [Properties →](./properties.md#computed-properties) | `+computedProp` |
1015
1129
 
1016
1130
  ---
1017
1131