Search selfstore
v1.8.21

Widgets overview

The screens for choosing where data lives, showing whether it is saved, managing backups, sharing and joining are the same in every app. selfstore/widgets ships them as custom elements: plain HTML you drop into any page, theme with CSS, and reword for your locale.

Start here

Most apps need one element and two lines:

<selfstore-storage></selfstore-storage>
const store = await selfstore('app', { drive: { clientId } });
document.querySelector('selfstore-storage').store = store;

<selfstore-storage> is the whole storage journey: it asks the first-run question when there is no home yet, and shows the panel once there is one. Which of the two, and when, follows the engine’s own status.

If that is what you needed, you are done - the rest of this section is for apps that place the pieces themselves.

They are skins over the same headless flows the API exposes. Every ordering and failure rule lives in the flow and its tests, so nothing here is a black box, and dropping down to the flow later costs you no capability.

Register them

import { selfstore } from 'selfstore';
import { defineSelfstoreWidgets } from 'selfstore/widgets';

defineSelfstoreWidgets();               // registers every element once
const store = await selfstore('my-app');

defineSelfstoreWidgets() is safe to call twice - it skips names already defined. It touches customElements, so it is browser-only: call it from client code, not during server rendering. Pass a prefix to register under your own tag names:

defineSelfstoreWidgets('acme');         // <acme-gate>, <acme-connect>, ...

The gate builds its own connect child and reads the prefix back off its own tag name, so a custom prefix keeps the pair together.

Register one, not nine

Naming defineSelfstoreWidgets is what pins all nine elements into your bundle - convenient, and the reason a small app ships widgets it never renders. Where size matters, register only what you place:

import { defineStatus } from 'selfstore/widgets';

defineStatus();                         // <selfstore-status>, and nothing else
Function Registers
defineConnect <selfstore-connect>
defineStatus <selfstore-status>
defineShare <selfstore-share>
defineJoin <selfstore-join>
defineBackups <selfstore-backups>
defineGate <selfstore-gate>, with the connect child it builds
defineDestination <selfstore-destination>, with the connect and status it composes
defineStorage <selfstore-storage>, and therefore both of the above
defineAccount <selfstore-account>, with the status row it composes

Each takes the same optional prefix, and each pulls in what it composes - so the three that build children are honest about their weight rather than breaking at runtime.

The pieces

Storage, if you place them yourself rather than letting <selfstore-storage> decide:

Element What it renders
<selfstore-gate> The first-run screen, before there is a durable home
<selfstore-connect> The journey itself: pick, authorize, resolve, set a password
<selfstore-destination> The panel once there is a home: where, when, and how to change it
<selfstore-account> The same answer at header size, two gestures instead of four
<selfstore-status> One line, or one dot: is the data saved, and where

Everything else, which no composer covers because it depends on your app:

Element What it renders
<selfstore-backups> Several named backups on one home
<selfstore-share> Hand out a link so others can read or edit
<selfstore-join> Open a share link someone sent

Import only the ones you mount. A store that must never be shared simply never imports share and join, and that code stays out of the bundle - which is the point of the sensitive-app posture.

Wire one

Elements are inert until wired. They render nothing until you assign the property that gives them something to drive - usually store:

<selfstore-connect id="connect"></selfstore-connect>
<script type="module">
  const el = document.getElementById('connect');
  el.targets = { file: true, drive: true };
  el.store = store;                     // assign LAST: this is what wires the flow
</script>

Order matters. Every setter that changes the journey rewires the flow, and the store is what makes wiring possible at all. Assign the shape first (labels, icons, targets, options), then store. The gate is stricter still: it reads its frame when it opens, so labels set after it is on screen reach the child but not the frame’s own title.

These are properties, not attributes - they take objects and functions, which an HTML attribute cannot carry. A handful of scalar knobs are also attributes; each element’s page lists which.

The five customization layers

Nothing here requires forking a widget:

  1. CSS custom properties - --selfstore-accent, --selfstore-radius and thirteen more. They cross shadow boundaries, so one declaration on a parent themes everything below.

  2. ::part() - every significant node carries a part name, so you restyle any piece without touching internals.

  3. Labels - every string is a key with a default. Widgets ship English and French; override a key to reword, or a whole map to translate.

  4. Attributes and properties - the full journey is the default; each knob removes or narrows a piece. icons is one of them, and defaultIcons is what it starts from: a glyph per destination kind - a cloud for a hosted drive, a document for a file on the device, a server for WebDAV, a bucket for object storage. Import it to override one and keep the rest, rather than redrawing a set:

    import { defaultIcons } from 'selfstore/widgets';
    
    el.icons = { ...defaultIcons, file: myOwnFileGlyph };
  5. Events - every widget emits bubbling, composed selfstore-* events so the host reacts without polling.

Fonts and colours are inherited, not imposed

The widgets set font: inherit and color: inherit on their host element, and their neutral tones are derived from currentColor with color-mix(). Panels use the Canvas and CanvasText system colours.

In practice: a widget on a dark page is dark, with no configuration, as long as your page sets a colour. What it does not inherit is your accent - that one is a deliberate --selfstore-accent, because a brand colour is a decision, not a default.

They respond to their own width

Each widget’s stack is a CSS container (container-type: inline-size), and the layout switches below 480px of the widget’s own width - not the viewport’s. A widget in a narrow sidebar stacks correctly on a wide screen, with no host CSS and no media query of yours.

TypeScript

The element classes are exported, so a querySelector can be typed and a subclass can extend one:

Export For
SelfstoreGateElement <selfstore-gate>
SelfstoreStorageElement <selfstore-storage>
SelfstoreDestinationElement <selfstore-destination>
SelfstoreAccountElement <selfstore-account>
SelfstoreConnectElement <selfstore-connect>
SelfstoreStatusElement <selfstore-status>
SelfstoreBackupsElement <selfstore-backups>
SelfstoreShareElement <selfstore-share>
SelfstoreJoinElement <selfstore-join>
FlowWidget The abstract base every widget extends
WidgetLabels Record<string, string> - the shape of a labels map
import type { SelfstoreGateElement } from 'selfstore/widgets';

const gate = document.querySelector<SelfstoreGateElement>('selfstore-gate');
gate?.targets = { file: true };

Registering under a custom prefix and extending a class are the same mechanism: defineSelfstoreWidgets('acme') registers the stock classes under your names, while customElements.define('acme-gate', class extends SelfstoreGateElement {}) lets you override behaviour. The gate finds its connect child by rewriting its own tag name, so a subclass pair keeps working as long as both share a prefix.

When the API fits better

Widgets are the common journeys rendered fast. When you want the connect journey inside your own components, drive selfstore/flows directly - the widgets are a thin skin over exactly that. Every element also exposes its flow for programmatic control, so you can mount the widget and still drive it from code.