Using the widgets in a framework
The widgets are custom elements, so every framework can render the tag. What differs between frameworks is how a value reaches the element, and that is the whole of the integration story.
This page is about mounting the widgets. Binding a store’s state to your own components is a different job, covered in framework bindings.
The rule that explains everything
An element takes values two ways:
- Properties are JavaScript. They carry objects, functions and real
booleans:
store,targets,options,labels,icons,confirmAction,qrProvider. - Attributes are HTML. They carry strings only, and each widget observes a
handful of scalar knobs:
armed,deferrable,variant,levels,link,recommended,advanced,with-*.
Anything that is not a string must go through a property. A framework that
writes attributes will stringify your object into [object Object] and the
widget will ignore it.
The boolean trap
This one is worth a paragraph because it fails silently and no test catches it.
The boolean attributes are parsed: deferrable="false" turns the knob off,
and the with-* attributes also accept off, no and 0. The boolean
properties are stored as given, with no coercion:
el.deferrable = false; // off
el.deferrable = 'false'; // ON - a non-empty string is truthy
So in any framework that assigns properties rather than attributes,
deferrable="false" in your template sets the property to the string
"false", which is truthy - and the escape hatch you meant to remove is still
on screen. Pass a real boolean:
<selfstore-gate deferrable={false} /> <!-- not deferrable="false" -->
Plain HTML
<selfstore-connect id="connect"></selfstore-connect>
<script type="module">
import { selfstore } from 'selfstore';
import { defineSelfstoreWidgets } from 'selfstore/widgets';
defineSelfstoreWidgets();
const store = await selfstore('my-app');
const el = document.getElementById('connect');
el.targets = { file: true, drive: true };
el.store = el.store ?? store; // assign store LAST
el.addEventListener('selfstore-connected', (e) => console.log(e.detail.outcome));
</script>
React
React 19 and later assign a property when the element has one, so JSX props work directly. Earlier versions write attributes, which stringifies objects. The ref pattern below works on every version, so it is the one worth learning:
import { useEffect, useRef } from 'react';
import { defineSelfstoreWidgets } from 'selfstore/widgets';
defineSelfstoreWidgets();
export function Connect({ store }) {
const ref = useRef(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
el.targets = { file: true, drive: true };
el.store = store; // last
const onDone = (e) => console.log(e.detail.outcome);
el.addEventListener('selfstore-connected', onDone);
return () => el.removeEventListener('selfstore-connected', onDone);
}, [store]);
return <selfstore-connect ref={ref} />;
}
TypeScript needs the tag declared once:
declare module 'react' {
namespace JSX {
interface IntrinsicElements {
'selfstore-connect': React.DetailedHTMLProps<
React.HTMLAttributes<HTMLElement>,
HTMLElement
>;
}
}
}
React before 19 does not map custom events to on* props either, which is the
second reason the ref pattern is the safe default.
Vue
Vue checks whether the key exists on the element and assigns the property when it does, so bindings work as written. Tell the compiler the tag is a custom element so it stops warning about an unknown component:
// vite.config.js
export default {
plugins: [
vue({
template: {
compilerOptions: {
isCustomElement: (tag) => tag.startsWith('selfstore-')
}
}
})
]
};
<template>
<selfstore-connect
:targets="{ file: true, drive: true }"
:store="store"
@selfstore-connected="onConnected"
/>
</template>
Add the .prop modifier if you ever need to force the property path:
:targets.prop="targets".
Svelte
Svelte assigns properties on custom elements, so objects pass through untouched
and event listeners work with on::
<script>
import { defineSelfstoreWidgets } from 'selfstore/widgets';
defineSelfstoreWidgets();
export let store;
let gate;
$: if (gate) {
gate.targets = { file: true, drive: true };
gate.store = store; // last
}
</script>
<selfstore-gate
bind:this={gate}
armed={booted}
deferrable={false}
on:selfstore-gate-deferred={() => console.log('device-only for this session')}
>
<div slot="brand"><AppLogo /></div>
<div slot="footer"><AppFooter /></div>
</selfstore-gate>
Because Svelte assigns properties, this is exactly where the boolean trap bites:
write deferrable={false}, never deferrable="false".
Angular
Add CUSTOM_ELEMENTS_SCHEMA to the module or component, then bind properties
with [prop] and listen with (event):
@Component({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `
<selfstore-connect
[targets]="targets"
[store]="store"
(selfstore-connected)="onConnected($event)"
></selfstore-connect>
`
})
Events
Every widget event bubbles and is composed, so it crosses the shadow boundary and you can listen on an ancestor rather than on each element:
document.addEventListener('selfstore-connected', (e) => {
console.log(e.detail.outcome);
});
The full list, with the shape of each detail, is on the
events page.
Server rendering
defineSelfstoreWidgets() touches customElements, which does not exist on the
server. Call it from client-side code only:
if (typeof window !== 'undefined') defineSelfstoreWidgets();
The markup itself is safe to render on the server - an unregistered custom element is an inert unknown tag, and it upgrades as soon as the definition lands in the browser. Two things follow:
- give the element a size or a placeholder if a layout shift on upgrade would be visible;
- do not expect any widget content in the server HTML - the widgets render into
a shadow root, in the browser, after you assign
store.