Welcome to Vanilla Breeze
This bell pulls live notifications from /go/notify/messages — the same contract documented at /docs/concepts/service-contracts/. Static articles like this one are the no-JS / no-backend fallback.
This bell pulls live notifications from /go/notify/messages — the same contract documented at /docs/concepts/service-contracts/. Static articles like this one are the no-JS / no-backend fallback.
How VB components support both declarative HTML markup and reactive property assignment without forcing a choice.
VB components accept the same data through two doors. Author them in HTML and they work without a single line of script. Drive them from a reactive framework via .items and .data and they diff against existing markup, preserving in-flight state. Either mode is valid; you can mix them.
Pure HTML-first components are great for static pages, server rendering, and progressive enhancement, but they fight back when a framework re-renders the same DOM. Pure JS-first components are convenient for SPAs but require ceremony to ship anything visible before hydration. VB picks both: the same element accepts declarative children at upgrade and property assignments after — without a separate hydration step.
.columns / .items / .data. The component renders accordingly.Each component declares one of two roles in its data API.
.dataOne element represents one logical record. Examples: <work-item>, <user-story>. The .data property accepts a plain object whose fields map to the component's state attributes and slotted children. Reading .data returns the current shape.
const wi = document.createElement('work-item');wi.data = { itemId: 'PROJ-42', type: 'task', priority: 'high', title: 'Wire OAuth flow',};document.body.appendChild(wi); console.log(wi.data); // { itemId: 'PROJ-42', type: 'task', priority: 'high', title: '...' }/code-block <p>Setting <code>.data</code> is idempotent (shallow-equal short-circuit) and emits <code><localName>:data-changed</code> with <code>{ data, source: 'property' }</code>.</p> <h3>Collection components — <code>.items</code></h3> <p>One element manages a keyed list of children. Examples: <code><kanban-board></code>, and (forthcoming) <code><calendar-wc></code>, <code><chart-wc></code>, <code><gantt-chart></code>. The <code>.items</code> setter runs a keyed diff: existing nodes whose key persists are <strong>moved, never recreated</strong>, so in-flight drag, focus, and CSS animations survive across updates.</p> <code-block language="javascript" label="Collection contract" data-escape>const board = document.querySelector('kanban-board'); board.columns = [ { id: 'todo', label: 'To Do', wip: 5 }, { id: 'doing', label: 'In Progress', wip: 3 }, { id: 'done', label: 'Done' },]; board.items = [ { id: 'PROJ-1', column: 'todo', title: 'Wire auth', type: 'task' }, { id: 'PROJ-2', column: 'doing', title: 'Refactor cache', type: 'feature' },]; // A second assignment diffs against the previous list — same keys are reused.board.items = [ { id: 'PROJ-1', column: 'doing', title: 'Wire auth', type: 'task' }, // moved column { id: 'PROJ-2', column: 'doing', title: 'Refactor cache', type: 'feature' }, { id: 'PROJ-3', column: 'todo', title: 'New thing', type: 'task' }, // new key];
VB's collection diff makes a formal promise: any element whose key is present in both the previous and the next .items list is never destroyed and recreated. The same DOM node may be moved across containers or reordered within one, but its identity — and any state held against that identity — is preserved.
This matters for:
The diff is keyed by an id field on each item by default. Components can override with a static keyOf.
VB components fire <localName>:upgraded exactly once after their initial setup() succeeds. Frameworks should wait on this event before assigning .items or .data from outside, to avoid races where a property arrives before the element's internal structure is ready.
const board = document.querySelector('kanban-board'); if (!board.hasAttribute('data-upgraded')) { await new Promise(resolve => board.addEventListener('kanban-board:upgraded', resolve, { once: true }) );} board.items = visibleItems.get();
Reactive systems often listen to a component's own change events to update external state. Without a source tag, programmatic updates trigger a feedback loop: assignment → event → handler updates state → state pushes new .items → event again.
VB collection components emit <localName>:items-changed with a source field. Filter on it:
board.addEventListener('kanban-board:items-changed', (e) => { // Only react to changes the user made by dragging — not echoes from .items = ... if (e.detail.source === 'drag') { saveToServer(e.detail.items); }});
Possible source values:
api — set programmatically via .items = ...drag — user dragged a card to a new column or positionattribute — set via attribute mutation (rare for collections)internal — component-internal mutation.renderItemCollection components ship with a default item renderer that reproduces today's markup. To take over rendering — say, to inject your design system's card or wrap the work-item with extra chrome — assign a function to .renderItem.
board.renderItem = (item) => { const card = document.createElement('article'); card.className = 'my-custom-card'; card.dataset.id = item.id; card.draggable = true; card.innerHTML = `<h3>${item.title}</h3><span>${item.assignee}</span>`; return card;};board.items = [...];
The renderer must return an Element. Strings and tagged templates are not accepted in v1 — see issue VB-XG5X for the open decision.
VB exposes the building blocks under src/lib/:
VBElement — minimal base class with setup() / teardown() lifecycle and :upgraded event.VBRecord — mixin that adds .data with attribute reflection per a static dataSchema.VBCollection — mixin that adds .items with the keyed diff and the preservation guarantee.diffByKey — the pure reconciler under VBCollection; useable directly if you want the algorithm without the mixin.import { VBElement } from 'vanilla-breeze/lib/vb-element.js';import { VBCollection } from 'vanilla-breeze/lib/vb-collection.js'; class MyList extends VBCollection(VBElement) { static keyOf = (item) => item.uuid; setup() { // Build any shell DOM here. Inherited :upgraded fires after this returns. } _renderItem(item) { const li = document.createElement('li'); li.dataset.id = item.uuid; li.textContent = item.label; return li; } // Optional: route items into specific child containers. // _containerFor(item, existing) { return this.querySelector(`#col-${item.column}`); } // Optional: refresh placeholders / counts after every diff. // _postRender({ added, moved, removed, items }) { ... }} customElements.define('my-list', MyList);
renderItem. v1 ships Element-only to avoid a parse step on every diff. We'll revisit after pilot consumers report their needs.<color-picker>, <date-picker>, <combo-box>, etc. expose .value as their canonical property; they do not get .data bolted on.fetch / framework resources and feed the result into .items or .data.<kanban-board> — the pilot collection component<work-item> — record component used by kanban<user-story> — record component used by kanban