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

base

Page background — birch

layer

Cards, panels — mist

overlay

Modals, dropdowns — sand

featured

Hero sections — void

Dark theme

base

Page background — void

layer

Cards, panels — iron

overlay

Modals, dropdowns — slate

featured

Hero 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

base

Page background

layer

Cards, panels

overlay

Modals, dropdowns

featured

Always 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.

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:

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:

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-surfaceLight --color-bgDark --color-bgLight --color-bg-panelDark --color-bg-panelUse
base--bm2-birch --in0-void inheritsinheritsResets to page bg inside a deeper surface
layer--bm1-mist --in1-iron --bm2-birch--in2-slateCards, sidebars, panels
overlay--bm0-sand --in2-slate inherits--in2-slate (capped)Modals, dropdowns, floating elements
featured--in0-void --bm1-mist --in1-iron--bm2-birchHero 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

TokenLightDarkUse
--color-bg--bm2-birch--in0-voidPage background
--color-bg-panel--bm1-mist--in1-ironCards, panels
--color-bg-inset--bm0-sand--in2-slateInput fills, sunken areas
--color-bg-code--fo3-deepwater--in1-ironCode block background
--color-bg-interactive-hover--bm1-mist--in3-ashRow hover, ghost button fill

Text

TokenLightDarkUse
--color-text--in0-void--bm2-birchPrimary text
--color-text-secondary--in2-slate--bm1-mistSupporting text
--color-text-tertiary--in3-ash--bm0-sandPlaceholders, captions (AA Large only)

Borders

TokenLightDarkUse
--color-border--in3-ash--in2-slateDefault border
--color-border-strong--in1-iron--bm0-sandProminent dividers
--color-border-subtlergba 55,65,81 12%rgba 75,85,99 25%Hairline borders
--color-ghost-borderrgba 55,65,81 25%rgba 247,246,243 25%Ghost button ring

Accent & State

TokenLightDarkUse
--color-accent--fo1-fern--fo0-gladeCTAs, links, focus ring
--color-accent-hover--fo2-forest--fo1-fernHover state
--color-accent-active--fo3-deepwater--fo2-forestActive/pressed state
--color-accent-text--bm2-birch--in0-voidText on accent backgrounds
--color-error--bl0-ember--bl0-emberError, destructive
--color-warning--bl1-ochre--bl1-ochreWarning, draft
--color-success--bl3-moss--bl3-mossSuccess, published
--color-on-error--bm2-birch--bm2-birchText on error backgrounds
--color-on-success--bm2-birch--bm2-birchText on success backgrounds
--color-on-warning--bm2-birch--bm2-birchText on warning backgrounds