# Styling the widgets Source: https://selfstore.dev/docs/widgets-styling The complete CSS surface of selfstore's widgets - fifteen custom properties with their defaults, every ::part() name, how theming crosses shadow boundaries, container queries, and the one place ::part() cannot reach. 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. ```css /* 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()`: ```css 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 | | --- | --- | | `` | `tabs`, `presets`, `eye`, `advanced-link`, `forgot-link`, `webdav-help`, `webdav-signup`, `webdav-note`, `warn-note` | | `` | `gate`, `gate-card`, `gate-title`, `gate-foot`, `gate-defer`, `gate-defer-note` | | `` | `status-row`, `status-action`, `dot-button` | | `` | `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` | | `` | `qr` | | `` | `banner` | ## The one place `::part()` cannot reach `` builds its own `` **inside its shadow root**, and does not re-export that child's parts. So: ```css 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: ```css selfstore-gate { --selfstore-accent: #a8490a; /* reaches the connect inside */ --selfstore-radius: 10px; } ``` If you need `::part()` on the connect journey specifically, mount `` 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: ```css :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 ```css /* 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](https://selfstore.dev/docs/widget-gate) 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`](https://selfstore.dev/docs/advanced) and render it yourself - the widgets are a thin skin over exactly that API. Map of this site for a model: https://selfstore.dev/llms.txt Every page in one file: https://selfstore.dev/llms-full.txt