Surfaces
Farn's theming system has two orthogonal axes. data-theme sets an absolute light or dark context on any element. data-surface sets depth within that context — base, layer, overlay, or featured — and resolves automatically to the correct palette token for the current theme. They compose freely: a dark panel inside a light page needs only one attribute pair.
Live Demo
Both columns below are pinned to their respective themes. Each surface renders at the same depth so you can compare all four levels side by side.
Light theme
basePage background — birch
layerCards, panels — mist
overlayModals, dropdowns — sand
featuredHero sections — void
Dark theme
basePage background — void
layerCards, panels — iron
overlayModals, dropdowns — slate
featuredHero sections — deepwater
Responds to the page toggle
Without a forced data-theme the surfaces adapt automatically. Use the moon/sun button in the nav above to see all four levels update in place.
Current page theme
basePage background
layerCards, panels
overlayModals, dropdowns
featuredAlways dark
Override with data-theme
Add data-theme="dark" (or "light") to any element to pin it to a specific theme regardless of the page. Toggle the page theme — the right panel stays dark while the left follows the page.
data-surface="layer"Follows page theme
data-theme="dark" data-surface="layer"Always dark
<!-- Pinned theme + explicit depth -->
<section data-theme="dark" data-surface="layer">
Dark panel regardless of page theme
</section>Surface Card Pattern
A card that adapts to its surface with no component-specific CSS: background resolves fromdata-surface alone, and only --space-* / --radius-*semantic tokens shape it. Reuse this pattern instead of hand-rolling background/border rules per page — it's what the landing page's token story section is built from. Unlike the shipped .card component class, it has no fixedbackground — that's the point: it lets data-surface decide.
Copy-paste — Surface Card
<div class="surface-card" data-surface="layer">
<h4>Card title</h4>
<p>Card copy.</p>
</div>.surface-card {
padding: var(--space-lg);
border-radius: var(--radius-lg);
}No background declaration needed — data-surface sets it directly on the element. Add border: 1px solid var(--color-border) if adjacent surfaces need a visible edge (e.g. nested one level deep with little contrast between steps).
The Two-Axis Model
Most theming systems give you a single switch — light or dark. Farn gives you two independent controls: an absolute theme axis and a relative depth axis.
The absolute axis (data-theme) sets the entire context for an element and all its descendants. The relative axis (data-surface) expresses depth within whatever theme context is currently active. Because depth is relative, adata-surface="layer" on a dark panel resolves to Iron Night iron, and on a light panel it resolves to Birch Mist mist — the same semantic intent, the right palette value.
This is why Farn palettes are designed in complementary pairs: Iron Night and Birch Mist mirror each other's depth levels. Iron Night void = dark page; Birch Mist birch = light page. Iron Night iron = dark card; Birch Mist mist = light card. The surface system is an expression of the palette story, not a separate mechanism bolted on.
The featured surface opts out of theme-relativity by design. It always provides maximum contrast against the page — void in light mode, mist in dark mode — and forces its own text and border tokens so descendants inherit correct contrast without a data-theme attribute.
Featured Surface
The featured surface is intentionally theme-breaking. It always provides maximum contrast against the page — void (#0D1117) in light mode, mist (#E9E6DC) in dark mode — giving hero sections and feature callouts visual weight that adapts without losing contrast.
Unlike the three relative surfaces, it overrides text and border tokens directly so descendants automatically inherit the correct palette values. Just add data-surface="featured"— no data-theme composition required.
<section data-surface="featured">
<!-- light mode: void bg, dark-palette text (birch/mist/sand) -->
<!-- dark mode: mist bg, light-palette text (void/slate/ash) -->
</section>Toggle the page theme — the block below inverts to maximum contrast in both modes:
data-surface="featured"Light mode: void · Dark mode: mist
Light — Primary text 19:1 (AAA) · Secondary 9:1 (AAA) · Tertiary 5.9:1 (AA)
FOWT Prevention
A Flash of Wrong Theme (FOWT) occurs when the browser renders a frame in the default light mode before JavaScript runs and reads the stored preference. The fix is a tiny synchronous inline script placed in <head> — before <body> — that setsdata-theme on <html> before the browser paints anything:
<script>
(function () {
const stored = localStorage.getItem('farn-theme');
const system = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', stored ?? system);
})();
</script>Three rules for the script to work:
- Synchronous, not deferred. No
defer, notype="module". The script must block parsing briefly to run before the first paint. - In
<head>before<body>. Any content painted before the script runs will flash in the wrong theme. - localStorage key is
farn-theme. The theme toggle must write to the same key. Falls back toprefers-color-schemewhen no stored preference exists.
In Astro, use is:inline on the script tag to prevent bundling:<script is:inline>...</script>. For a practical setup walkthrough, see Getting Started › FOWT.
Token Reference
Surface Depth
Apply data-surface to set depth on any element. Tokens resolve automatically for the active theme.
data-surface | Light --color-bg | Dark --color-bg | Light --color-bg-panel | Dark --color-bg-panel | Use |
|---|---|---|---|---|---|
base | --bm2-birch | --in0-void | inherits | inherits | Resets to page bg inside a deeper surface |
layer | --bm1-mist | --in1-iron | --bm2-birch | --in2-slate | Cards, sidebars, panels |
overlay | --bm0-sand | --in2-slate | inherits | --in2-slate (capped) | Modals, dropdowns, floating elements |
featured | --in0-void | --bm1-mist | --in1-iron | --bm2-birch | Hero sections, feature callouts — maximum contrast |
Dark overlay ceiling: in dark mode --color-bg-panel inside an overlay is capped at --in2-slate — the same as the overlay background — because slate is the deepest available surface.
Theme Toggle
Set data-theme on any element to control its theme context. All semantic tokens re-declare inside [data-theme] — no extra overrides needed.
<!-- Page-level: set on <html> -->
<html data-theme="dark">
<!-- Element-level: local override -->
<section data-theme="dark">
Always dark, regardless of the page theme
</section>
<!-- Composed: pinned theme + explicit depth -->
<section data-theme="dark" data-surface="layer">
Dark panel, card depth
</section>Background
| Token | Light | Dark | Use |
|---|---|---|---|
--color-bg | --bm2-birch | --in0-void | Page background |
--color-bg-panel | --bm1-mist | --in1-iron | Cards, panels |
--color-bg-inset | --bm0-sand | --in2-slate | Input fills, sunken areas |
--color-bg-code | --fo3-deepwater | --in1-iron | Code block background |
--color-bg-interactive-hover | --bm1-mist | --in3-ash | Row hover, ghost button fill |
Text
| Token | Light | Dark | Use |
|---|---|---|---|
--color-text | --in0-void | --bm2-birch | Primary text |
--color-text-secondary | --in2-slate | --bm1-mist | Supporting text |
--color-text-tertiary | --in3-ash | --bm0-sand | Placeholders, captions (AA Large only) |
Borders
| Token | Light | Dark | Use |
|---|---|---|---|
--color-border | --in3-ash | --in2-slate | Default border |
--color-border-strong | --in1-iron | --bm0-sand | Prominent dividers |
--color-border-subtle | rgba 55,65,81 12% | rgba 75,85,99 25% | Hairline borders |
--color-ghost-border | rgba 55,65,81 25% | rgba 247,246,243 25% | Ghost button ring |
Accent & State
| Token | Light | Dark | Use |
|---|---|---|---|
--color-accent | --fo1-fern | --fo0-glade | CTAs, links, focus ring |
--color-accent-hover | --fo2-forest | --fo1-fern | Hover state |
--color-accent-active | --fo3-deepwater | --fo2-forest | Active/pressed state |
--color-accent-text | --bm2-birch | --in0-void | Text on accent backgrounds |
--color-error | --bl0-ember | --bl0-ember | Error, destructive |
--color-warning | --bl1-ochre | --bl1-ochre | Warning, draft |
--color-success | --bl3-moss | --bl3-moss | Success, published |
--color-on-error | --bm2-birch | --bm2-birch | Text on error backgrounds |
--color-on-success | --bm2-birch | --bm2-birch | Text on success backgrounds |
--color-on-warning | --bm2-birch | --bm2-birch | Text on warning backgrounds |