| name | obsidian-arrow-css |
| description | Use when styling Arrow.js components for an Obsidian plugin โ deciding between Obsidian's semantic classes, oas-* utility classes, and custom CSS; reading the live token and class references; writing scoped rules that win the specificity battle without leaking; using CSS custom properties for overrides; and auditing components to minimize hand-written CSS. |
Obsidian Arrow CSS
Practical guide to styling components so they look native in Obsidian with the
minimum amount of custom CSS. The goal: every rule you write is load-bearing โ
nothing exists that could have been replaced by an Obsidian class or a utility.
CSS prefix conventions
Three prefix families, three locations โ never mix them:
| Prefix | Location | Portability |
|---|
oasbox-* | tools/sandbox/sandbox.css | Sandbox chrome only โ never copy into a plugin |
oas-* | src/utilities.css | Portable โ copy with components into any plugin |
oas-shell-* | src/utilities.css (shell section) | Portable structural shells โ view and component roots |
If you see an oasbox-* class in component code, it is a mistake โ those classes exist only in the sandbox frame (viewer chrome) and won't be styled in a real plugin. Use oas-* utilities or scoped component CSS instead.
oasbox-* CSS classes vs the oasbox CLI โ not the same thing.
oasbox-* is the CSS prefix for sandbox chrome (frame, toolbar, sidebar) in
tools/sandbox/sandbox.css. The oasbox CLI is the separate in-project
daily-driver tool (oasbox generate, oasbox validate, etc.). The shared
project-name prefix is the only connection.
The decision hierarchy
Always go in this order:
1. Obsidian semantic class โ /reference/classes in the running sandbox
2. oas-* utility class โ src/utilities.css (ships with every scaffold)
3. Custom CSS โ last resort, must be scoped
If you find yourself writing display: flex in a style="" attribute or a CSS
rule, stop โ use oas-flex. If you're writing .my-thing { padding: 8px },
stop โ use oas-p-2. If Obsidian already has a class for the pattern, use it and
skip both.
1. Obsidian semantic classes โ /reference/classes
Open /reference/classes in the running sandbox for a live, theme-aware catalog.
Key classes that cover most plugin UI:
| Pattern | Class(es) |
|---|
| Settings row (label + control) | .setting-item + .setting-item-info + .setting-item-control |
| Section heading | .setting-item.setting-item-heading |
| Primary button | button.mod-cta |
| Danger button | button.mod-destructive |
| Warning button | button.mod-warning |
| Icon button | button.clickable-icon |
| Toggle | .checkbox-container (+ .is-enabled) |
| Vertical tab nav | .vertical-tab-nav-item (+ .is-active) |
| Modal card | .modal โ .modal-title + .modal-content |
| Fuzzy-finder row | .suggestion-item (+ .is-selected) |
| Callout block | `.callout[data-callout="info |
| Inline tag | .tag |
| Counter badge | .badge |
| File tree row | .nav-file-title (+ .is-active) |
| Folder row | .nav-folder-title |
| Frontmatter property | .metadata-container โ .metadata-property |
| Pane header | .view-header โ .view-header-title |
This is a sample โ browse /reference/classes for the full catalog with live
previews and whenToUse notes.
2. oas-* utility classes โ src/utilities.css
When layout/spacing is needed that Obsidian has no semantic class for, use these.
All values pull from Obsidian's CSS custom property scale:
Layout
oas-flex oas-inline-flex oas-flex-col oas-flex-wrap
oas-items-start oas-items-center oas-items-end oas-items-baseline
oas-justify-start oas-justify-center oas-justify-end oas-justify-between
oas-grow oas-shrink-0
Spacing โ two Obsidian token families:
4-px scale (N maps to --size-4-N): 1=4px, 2=8px, 3=12px, 4=16px, 5=20px, 6=24px
oas-gap-{1โ6}
oas-p-{1โ6} oas-px-{2โ6} oas-py-{1โ6}
oas-mt-{1โ6} oas-mb-{1โ6} oas-ml-auto
2-px sub-scale (class name mirrors Obsidian token name): 2-1=2px, 2-3=6px
oas-gap-2-1 oas-p-2-1 oas-px-2-1 oas-py-2-1 oas-mt-2-1 oas-mb-2-1
oas-gap-2-3 oas-p-2-3 oas-px-2-3 oas-py-2-3 oas-mt-2-3 oas-mb-2-3
Sizing
oas-w-full oas-min-w-0 oas-min-h-0
Typography
oas-text-xs oas-text-sm oas-text-md
oas-font-medium oas-font-semibold oas-font-mono oas-leading-1
oas-text-normal oas-text-muted oas-text-faint
oas-text-accent oas-text-success oas-text-error
Overflow
oas-truncate oas-overflow-hidden oas-overflow-auto
Border
oas-border oas-border-b oas-border-t
oas-rounded-s oas-rounded-m oas-rounded-l
Interaction
oas-cursor-pointer oas-select-none
Badges
oas-badge inline status/label badge
oas-badge.is-live success-tinted background (readable contrast)
oas-badge.is-draft muted background
Collapsible card
oas-card expandable section (add is-expanded to open)
oas-card.is-expanded
oas-card-header click target for toggle
oas-card-title truncating label text
oas-card-chevron rotating โบ indicator
oas-card-body hidden by default, shown when expanded
oas-card-desc muted description inside body
oas-card-note smaller footnote inside body
oas-card-children inline chip list inside body
oas-card-actions action row inside body
View & component shells
oas-shell-view root of a full-pane view (flex col, fills height)
oas-shell-view-header shrink-0 header
oas-shell-view-body flex-1 scrollable content area
oas-shell-view-footer shrink-0 pinned footer with top border
oas-shell-panel standalone component card (border, radius, padding)
Empty state (co-located in src/components/EmptyState/EmptyState.css โ ships with the component, not in utilities.css)
oas-empty-state flex column, centered, muted text
oas-empty-icon large faint icon above the heading
oas-empty-title bold normal-color heading
oas-empty-desc small muted description (max 280px)
oas-empty-action action area below description (margin-top)
Use the EmptyState component directly rather than applying these classes by hand.
Common compositions:
html`<div class="oas-flex oas-items-center oas-gap-2 oas-min-w-0">
<span class="oas-truncate oas-text-muted">${label}</span>
<span class="badge oas-shrink-0">${count}</span>
</div>`
html`<div class="oas-flex oas-flex-col oas-gap-3 oas-p-3">
${rows}
</div>`
3. Tokens โ /reference (the live token table)
Open /reference to see every var(--) property parsed live from app.css,
grouped by category (Size & spacing, Radius, Colors, Typography, โฆ) with color
swatches, size bars, resolved values, and a filter. Use the filter to find the
right token before reaching for a hardcoded value.
Common tokens by category:
| Category | Key tokens |
|---|
| Spacing (4-px scale) | --size-4-1 (4px) โ --size-4-6 (24px) |
| Spacing (2-px sub-scale) | --size-2-1 (2px), --size-2-3 (6px) |
| Radius | --radius-s, --radius-m, --radius-l, --radius-xl |
| Background | --background-primary, --background-secondary, --background-modifier-border, --background-modifier-hover |
| Text | --text-normal, --text-muted, --text-faint, --text-accent, --text-error, --text-success |
| Interactive | --interactive-accent, --interactive-normal, --interactive-hover |
| Font size | --font-ui-smaller, --font-ui-small, --font-ui-medium |
| Font weight | --font-medium, --font-semibold, --font-bold |
| Font family | --font-interface, --font-monospace |
Always use tokens in custom CSS โ never hardcode colors, sizes, or font values.
Token values are resolved at runtime against the active theme:
.my-thing { color: #888; padding: 8px; font-size: 12px; }
.my-frame .my-thing { color: var(--text-muted); padding: var(--size-4-2); font-size: var(--font-ui-small); }
4. Writing scoped custom CSS (when you must)
Obsidian's global rules have specificity (0,1,1) โ e.g.
button:not(.clickable-icon) beats a plain .my-btn (0,1,0).
Always scope under container class + element type:
.my-action { background: var(--interactive-accent); }
.my-panel button.my-action { background: var(--interactive-accent); }
Rules:
- Sandbox-only chrome (toolbar, frame, sidebar) โ
tools/sandbox/sandbox.css
- Component-specific overrides โ a co-located
MyThing.css imported in main.ts,
or scoped rules in sandbox.css
- Never add rules that target unscoped element types โ they will leak
Verify ancestor classes are applied in templates
When CSS uses a class as an ancestor selector (.my-shell .descendant { โฆ }), verify
that .my-shell is actually applied to a component root in at least one Arrow template.
A CSS file can be syntactically valid, imported with no errors, and still have zero effect
if the ancestor class is never mounted. This is not caught by the orphaned-CSS lint
(which only checks if the file has importers) โ it requires either pnpm check:scope
(automated) or a manual grep:
grep -r "my-shell" src/ --include="*.ts"
If the grep returns nothing, the CSS layer is silently inert.
5. Overrides and variables
If a group of components shares a visual parameter that might change (e.g. a panel
width, an accent color variant, a custom border radius), define it as a CSS custom
property on a container, not as a magic number scattered across rules:
.my-panel {
--my-panel-width: 280px;
--my-panel-accent: var(--interactive-accent);
}
.my-panel .my-panel-header {
width: var(--my-panel-width);
border-left: 3px solid var(--my-panel-accent);
}
To override for a variant, add a modifier class and redefine the variable โ no
duplicate rules:
.my-panel.is-wide { --my-panel-width: 480px; }
6. Auditing for excess custom CSS
Before calling a component done, ask for each custom rule:
- Could an Obsidian semantic class replace this? Check
/reference/classes.
- Could an
oas-* utility replace this? Check the list in ยง2.
- Is this value a hardcoded number or color? Replace with an Obsidian token.
- Does this rule use a bare element selector? Add a container prefix.
- Is this rule duplicated? Extract a shared variable or a common utility.
If all five pass, the rule belongs.
Porting CSS to the plugin โ pnpm port:css
When porting to the real plugin, class names must be prefixed to avoid collisions.
The port:css command handles this automatically at port time (never modifies
source files). Configure porting.config.json at the project root:
{
"cssPrefix": "my-plugin-",
"viewSubScope": true,
"include": ["src/components/**/*.css", "src/views/**/*.css"],
"outDir": "port-output/css"
}
Run pnpm port:css as the final step before copying into the plugin. Output lands
in outDir/ with an index.css that @imports everything. See the
arrow-js-obsidian-porting skill for the full reference including viewSubScope
behavior and how ancestor selectors are derived.
Keeping utilities.css in sync across ports
When you update src/utilities.css in the sandbox (add a utility, rename a
class), any plugin that has already ported it needs the new version. Track this
the same way as component drift: use the arrow-js-obsidian-porting skill's
parity-check approach, or include utilities.css in the project's
port-parity.json manifest so CI catches divergence.