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.