Search selfstore
v1.8.21

Styling the widgets

The widgets carry structure and behaviour, not a look. Everything visual routes through CSS you write in your own stylesheet - there is no theme object, no config file, and no build step.

Two levers, in this order: custom properties for anything you can express as a token, ::part() for anything you cannot.

Custom properties

These cross shadow boundaries, which is what makes them the primary lever: one declaration on a common ancestor themes every widget below it, however deeply nested.

/* Themes every selfstore widget on the page. */
:root {
  --selfstore-accent: #a8490a;
  --selfstore-radius: 10px;
}
Property Default Affects
--selfstore-accent #2563eb Primary buttons, focus rings, links, active states
--selfstore-accent-contrast #ffffff Text drawn on top of the accent
--selfstore-muted color-mix(in srgb, currentColor 55%, transparent) Secondary text: subtitles, hints
--selfstore-border color-mix(in srgb, currentColor 16%, transparent) Card, field and row borders
--selfstore-radius 12px Corner radius of cards, fields and buttons
--selfstore-gap 0.6rem Vertical rhythm between stacked elements
--selfstore-ok #16a34a The “saved” severity: status dot and pills
--selfstore-warn #d97706 The “needs attention” severity
--selfstore-danger #dc2626 Destructive buttons and error text
--selfstore-icon-size 2.1em Destination icons on connect cards and status rows
--selfstore-qr-size 6.5em The QR image on a share link card
--selfstore-gate-width 30rem Max width of the gate’s card
--selfstore-gate-title-size 1.5rem The gate’s title
--selfstore-gate-backdrop Canvas What sits behind the gate, covering the app
--selfstore-gate-z 280 The gate’s z-index, when your app has its own layers

Only --selfstore-accent and --selfstore-accent-contrast are true brand decisions. The rest have defaults that already work; set them when your design system says something different.

Parts

Every significant node carries a part name. Style them from your stylesheet with ::part():

selfstore-connect::part(card) {
  border-width: 2px;
}
selfstore-connect::part(button-primary) {
  font-weight: 700;
  letter-spacing: 0.01em;
}

A node can carry several part names

Buttons are the clearest case: a primary button is part="button button-primary". So ::part(button) reaches every button, and ::part(button-primary) reaches only the primary ones. Style the general case once, then override the specific.

The same pattern applies to card / card-active, row / row banner, status / status-ok, link / link-danger, and hint / warn-note.

Shared across widgets

Part Node
title The widget’s own heading
sub A secondary line under a title or row
hint An explanatory note
card A bordered block: a destination, a backup, a form
row A horizontal line of content
list A list container
button Any button
button-primary The button that advances the journey
button-danger A destructive button
link A text button styled as a link
link-danger A destructive text button
input A text field
label A field’s visible label
labelled The label-and-field pair
field A form field wrapper
icon A destination icon
tag A small badge, such as “Recommended”
status A status line
status-ok, status-error Its severity variants
error-note An error message
spinner The busy indicator
footer A widget’s foot

Per widget

Widget Parts
<selfstore-connect> tabs, presets, eye, advanced-link, forgot-link, webdav-help, webdav-signup, webdav-note, warn-note
<selfstore-gate> gate, gate-card, gate-title, gate-foot, gate-defer, gate-defer-note
<selfstore-status> status-row, status-action, dot-button
<selfstore-backups> card-active, menu, menu-button, menu-layer, menu-backdrop, new-button, open-row, open-shared, eye, replica-label, replica-form, replica-picker, replica-dest, replica-line, replica-ok, replica-error, replica-remove
<selfstore-share> qr
<selfstore-join> banner

The one place ::part() cannot reach

<selfstore-gate> builds its own <selfstore-connect> inside its shadow root, and does not re-export that child’s parts. So:

selfstore-gate::part(gate-card)   { /* works */ }
selfstore-gate::part(card)        { /* does NOT reach the connect cards */ }

Custom properties are unaffected - they inherit through every shadow boundary - so theming the gate’s destination cards works exactly as it does anywhere else:

selfstore-gate {
  --selfstore-accent: #a8490a;   /* reaches the connect inside */
  --selfstore-radius: 10px;
}

If you need ::part() on the connect journey specifically, mount <selfstore-connect> yourself instead of the gate, and decide when to show it.

Dark mode comes free

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

So a widget inside a dark page is dark, with no configuration - provided your page actually sets a color on an ancestor. The one thing to check when you switch themes is your accent’s contrast against the new ground:

:root {
  --selfstore-accent: #a8490a;
}
@media (prefers-color-scheme: dark) {
  :root {
    --selfstore-accent: #f59e0b;
    --selfstore-accent-contrast: #1c1305;
  }
}

They respond to their own width, not the viewport’s

Each widget’s stack declares container-type: inline-size, and the internal layout switches below 480px of the widget’s own width. A widget dropped into a 320px sidebar stacks correctly on a 4K screen, and you write no media query.

Two consequences worth knowing: sizing the widget’s container is enough to change its layout, and a widget will not react to a viewport breakpoint you define - it reacts to the box you give it.

A worked example

/* One block, every widget on the page. */
:root {
  --selfstore-accent: #a8490a;
  --selfstore-accent-contrast: #ffffff;
  --selfstore-radius: 10px;
  --selfstore-gap: 0.7rem;
}

/* Match the app's own button shape. */
selfstore-connect::part(button),
selfstore-backups::part(button) {
  font-weight: 650;
  padding-inline: 1.15rem;
}

/* The gate is a full-screen moment: give it the app's paper, not the system canvas. */
selfstore-gate {
  --selfstore-gate-backdrop: #faf9f7;
  --selfstore-gate-width: 34rem;
}

What you cannot do

There is no way to reorder or remove an internal node with CSS, and no slot for injecting markup mid-journey (the gate’s three slots are the exception, and they sit around the journey, not inside it). If a screen needs a different shape rather than a different look, that is the signal to drive selfstore/flows and render it yourself - the widgets are a thin skin over exactly that API.