Surfaces
stable
Farn's theming system has two orthogonal axes. data-theme sets an absolute
light or dark context on any element. data-surface sets a relative depth
within that context — base, layer, or overlay — 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.
For the token lookup table and copy-ready snippets, see Styles › Theming.
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, a
data-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.
Depth Reference
Each surface overrides --color-bg (and where relevant --color-bg-panel)
so that any element using background: var(--color-bg) picks up the right depth
automatically.
data-surface | Light --color-bg | Dark --color-bg | Use |
|---|---|---|---|
base | --bm2-birch | --in0-void | Resets to page bg; useful inside a deeper surface when you need to escape back up |
layer | --bm1-mist | --in1-iron | Cards, sidebars, panels — one step raised from the page |
overlay | --bm0-sand | --in2-slate | Modals, dropdowns, floating elements — highest depth level |
Live Demo
The two columns below are forced to their respective themes via data-theme on the
column wrapper. Each surface renders as a sibling block so you can compare all three depths
side by side in both themes simultaneously.
Light theme
base Page background — birch
layer Cards, panels — mist
overlay Modals, dropdowns — sand
Dark theme
base Page background — void
layer Cards, panels — iron
overlay Modals, dropdowns — slate
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 three levels update in place.
Current page theme
base Page background
layer Cards, panels
overlay Modals, dropdowns
Composition
Combine data-theme and data-surface on the same element to create a
surface that is pinned to a specific theme regardless of the page. Both panels below stay constant
as you toggle the page theme.
data-theme="dark" data-surface="layer" Always a dark panel
data-theme="light" data-surface="layer" Always a light panel
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. On a fast connection it is a brief flicker; on a slow one it can persist for a noticeable moment.
The fix is a tiny synchronous inline script placed in <head> — before
<body> — that sets data-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 execute before the first paint, which means it must block parsing briefly. - 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.