# Using the widgets in a framework
Source: https://selfstore.dev/docs/widgets-frameworks
Mounting selfstore's custom elements in React, Vue, Svelte, Angular and plain HTML - the property-versus-attribute rule, the boolean trap that silently keeps a knob on, listening to events, and server rendering.
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](https://selfstore.dev/docs/frameworks).
## 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:
```ts
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:
```svelte
```
## Plain HTML
```html
```
## 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:
```tsx
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 ;
}
```
TypeScript needs the tag declared once:
```ts
declare module 'react' {
namespace JSX {
interface IntrinsicElements {
'selfstore-connect': React.DetailedHTMLProps<
React.HTMLAttributes,
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:
```js
// vite.config.js
export default {
plugins: [
vue({
template: {
compilerOptions: {
isCustomElement: (tag) => tag.startsWith('selfstore-')
}
}
})
]
};
```
```vue
```
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:`:
```svelte
console.log('device-only for this session')}
>
```
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)`:
```ts
@Component({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `
`
})
```
## 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:
```ts
document.addEventListener('selfstore-connected', (e) => {
console.log(e.detail.outcome);
});
```
The full list, with the shape of each `detail`, is on the
[events](https://selfstore.dev/docs/events) page.
## Server rendering
`defineSelfstoreWidgets()` touches `customElements`, which does not exist on the
server. Call it from client-side code only:
```ts
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`.
Map of this site for a model: https://selfstore.dev/llms.txt
Every page in one file: https://selfstore.dev/llms-full.txt