SuperAPI AI Directory

obsidian-arrow-css

Frontend & websites

View repository

Updated 10-15 23:48

About this skill

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:

// Flex row with gap, centered, truncated label
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>`

// Full-width column with spacing
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:

/* ❌ hardcoded — wrong theme, wrong density */
.my-thing { color: #888; padding: 8px; font-size: 12px; }

/* ✓ token-resolved — correct for the active theme */
.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:

/* (0,1,0) — LOSES to Obsidian's button rule */
.my-action { background: var(--interactive-accent); }

/* (0,2,1) — WINS, and can't leak */
.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:

/* Define on the container */
.my-panel {
    --my-panel-width: 280px;
    --my-panel-accent: var(--interactive-accent);
}

/* Use in descendants */
.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:

  1. Could an Obsidian semantic class replace this? Check /reference/classes.
  2. Could an oas-* utility replace this? Check the list in §2.
  3. Is this value a hardcoded number or color? Replace with an Obsidian token.
  4. Does this rule use a bare element selector? Add a container prefix.
  5. 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-",     // prepended to every class selector in output
  "viewSubScope": true,          // view CSS gets ancestor selector for leaf isolation
  "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.