Styling: the user's theme wins¶
ZettelFlow has no look of its own. It borrows Obsidian's, and the reason is not modesty — it is that the user chose a theme and we do not know which one. A vault runs Minimal, or Things, or AnuPpuccin, or a snippet the user wrote last Tuesday. Every colour we hardcode is a colour that survives their choice, and every pixel we invent is one their theme cannot reach.
Obsidian says this plainly, and the quotes below are the rule rather than a paraphrase of it.
What Obsidian says¶
"Don't do this [inline styles]… use CSS classes, as hardcoding the styling in the plugin code makes it impossible to modify with themes and snippets." — Plugin guidelines
"To make the styling of your plugin consistent with Obsidian and other plugins you should use the CSS variables provided by Obsidian." — Plugin guidelines
"If you use these variables for your styles, your plugin will look great even if the user has a different theme! 🌈" — HTML elements
"Any text in UI elements should be using Sentence case instead of Title Case." "Using the heading elements from HTML will result in inconsistent styling between different plugins" — use
setHeading(). And: "Avoid including the word 'settings' to these headings." — Plugin guidelines
The variables to reach for¶
Obsidian groups them as Foundations (spacing, radiuses, colors, typography, borders, cursor, icons, layers), Components (button, checkbox, text input, toggle, slider, multi-select, modal, popover, dialog, tabs, navigation), Editor, Plugins and Window (reference).
Spacing — a 4-pixel grid. "Obsidian uses a 4-pixel grid to structure UI elements."
--size-4-1 … --size-4-18 | 4, 8, 12, 16, 20, 24, 32, 36, 48, 64, 72 px — padding and margin come from here |
--size-2-1 · --size-2-2 · --size-2-3 | 2, 4, 6 px — "sparingly and only when you need more fine-grained spacing" |
Colour — never a hex.
| what you mean | what to write |
|---|---|
| the accent, and anything interactive | --interactive-accent, --interactive-accent-hover, --interactive-normal, --interactive-hover, --color-accent |
| surfaces | --background-primary, --background-primary-alt, --background-secondary, --background-secondary-alt |
| borders, hovers, form fields | --background-modifier-border, --background-modifier-border-hover, --background-modifier-border-focus, --background-modifier-hover, --background-modifier-form-field |
| text | --text-normal, --text-muted, --text-faint, --text-on-accent, --text-accent |
| something went well, or wrong | --text-success, --text-warning, --text-error, --background-modifier-success, --background-modifier-error |
Shape. --radius-s, --radius-m, --radius-l, and --button-radius for a button (Button reference).
The rules, as this repo applies them¶
- No hex, no
rgb(), no named colour in a stylesheet. There is exactly one exemption, and it is documented in the file that holds it: the 3D graph draws over a fixed dark background of its own, where a theme's--text-faintis near-black and invisible. That file says so at the palette. - No pixel that the 4-grid can express.
padding: 8pxisvar(--size-4-2). A genuine pixel — a hairline, a sprite size, a WebGL dimension — is allowed and says why in a comment. - No styling from JavaScript. No
el.style.*, noinnerHTML. A class, always — that is what a snippet can reach. Enforced bylint:obsidian. - Reuse Obsidian's own classes before inventing one:
mod-ctafor the primary action,mod-warningfor a destructive one,clickable-iconfor an icon button,setting-itemand friends in settings,is-activefor a selected state. A button that looks like Obsidian's button is Obsidian's button. - Settings are built with the
SettingAPI andsetHeading(), never with hand-rolled headings — and no heading says "settings". - Sentence case, everywhere, in both locales.
- A shape that exists twice is a mixin.
src/styles/utils/mixins.scssis where a chip, a card, a row, a hint and an empty state are defined once; a partial that redefines one is drift.
The vocabulary (#546)¶
Rule 7 was written before it was true. src/styles/utils/variables.scss and src/styles/utils/mixins.scss were empty files that main.scss imported for the whole life of the project, which is why an audit of fifty partials found thirteen hand-drawn pill shapes with three radii, four paddings, two hovers, and an active state written out longhand in three files.
They are filled now, and nothing in them is a look of our own:
| file | what it holds |
|---|---|
utils/variables.scss | $space-*, $radius-*, $line-quiet, $surface-*, $text-*, $accent-* — every one an alias for an Obsidian variable. The name says which shape it is for; the value stays whatever the theme made it. |
utils/mixins.scss | the five shapes the audit counted: chip (+ chip-active, chip-clickable), card, row, hint, empty-state. |
Two exceptions are named rather than hidden. $radius-pill is 1em — no Obsidian variable expresses a pill, and em at least follows the font size the theme chose (999px in thirteen files followed nothing). $line-quiet carries the one genuine hairline, declared in the grid ratchet's table so it cannot hide among the debt.
To use them, a partial opens with:
and then says @include chip; rather than saying it again. Where a caller genuinely differs — the 3D graph paints over its own dark scene, a query chip is filled with the accent because it is a choice you made — it includes the mixin and overrides the one property that differs, so the difference shows up in the diff instead of hiding in a re-declaration.
test/styles/styleVocabulary.test.ts holds the line: no border-radius: 999px anywhere, no chip block that draws its own border and radius without the mixin, and no second way of saying this one is on. Its two siblings hold the colour (themeColours.test.ts, at zero outside the 3D graph) and the grid (themeGrid.test.ts, a ratcheting per-file ceiling that may only go down — eight of them came down the day the vocabulary landed).
Why this is a rule and not a preference¶
A theme is a promise the user made to themselves about how their vault looks. A plugin that hardcodes #f9a8d4 breaks that promise in a way the user cannot fix — not with a snippet, not with a different theme, not at all. The cost of the rule is that we cannot have a look of our own. That is the point: the look belongs to the user, and our job is to be legible inside it.
Recorded as constitution §XV.