Getting Started
Farn ships as a single CSS file — no build step, no runtime, no framework assumptions. Add one <link> tag or install from npm and you have all tokens.
For AI Agents
Farn ships an llms.txt at the site root — a concise machine-readable reference covering install, core rules, token layers, and links to every docs page. Drop the sample prompt below into your system context before generating any Farn CSS:
This project uses the Farn design system.
- Use semantic tokens (--color-*, --space-*), never palette tokens (--in*, --bm*, --fo*, --bl*)
- Every Fraunces heading: font-variation-settings: 'opsz' <value>
- data-theme="dark|light" on <html>, data-surface="base|layer|overlay" on elements
Full reference: https://farn.jbpt.de/llms.txtTip: For best results, allow your AI agent network access to farn.jbpt.de so it can fetch the full reference automatically.
npm Install
npm install farn-themeThen import in your bundler entry (Vite, webpack, Parcel, etc.):
@import "farn-theme"; /* full bundle: tokens + base reset */
@import "farn-theme/tokens"; /* tokens only — no reset */
@import "farn-theme/components"; /* opt-in component classes */
@import "farn-theme/typography"; /* opt-in typography utility classes */
@import "farn-theme/layout"; /* opt-in layout composition primitives */Five named export paths are available. Load the core bundle first ("farn-theme" or farn-theme/tokens), then opt-in bundles in order: components, typography, layout. The default ("farn-theme") includes the base reset; use farn-theme/tokens if you manage your own.
No bundler? Use the CDN path below — a <link> tag requires no build step.
CDN Install
Link directly from jsDelivr. Replace @0.7.0 with the latest version tag.
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/jabopiti/farn-theme@0.7.0/dist/farn.css">dist/farn.css is a single concatenated file containing palette tokens, semantic tokens, typography, spacing, motion tokens, and the base reset.
If you manage your own CSS reset, use the tokens-only bundle instead:
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/jabopiti/farn-theme@0.7.0/dist/farn-tokens.css">dist/farn-tokens.css includes all tokens but omits base.css (box-sizing reset, smooth scroll, focus ring, reduced-motion guard).
Load opt-in bundles after the core. Both require farn.css or farn-tokens.css for semantic tokens:
<!-- opt-in: component classes (buttons, cards, forms, badges, breadcrumbs, accordion) -->
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/jabopiti/farn-theme@0.7.0/dist/farn-components.css">
<!-- opt-in: typography utility classes -->
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/jabopiti/farn-theme@0.7.0/dist/farn-typography.css">
<!-- opt-in: layout composition primitives (.container, .stack, .cluster, .grid-2…) -->
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/jabopiti/farn-theme@0.7.0/dist/farn-layout.css">Replace @0.7.0 with the latest release tag.
What Gets Loaded
farn.css bundles seven layers in order:
colors.css— 17 palette tokens across four palettestypography.css— font-family tokens and scale referencespacing.css— space scale, layout widths, border radius, z-indexmotion.css— duration scale and easing tokensdark-light.css— semantic token layer with dark/light switchingcomponents.css— Tier-3 component tokens (--btn-*,--card-*,--input-*, etc.)base.css— box-sizing reset, smooth scroll, focus ring, reduced-motion
farn-tokens.css is the same bundle without base.css (layers 1–6 only).
Fonts are not bundled — load them separately. Self-hosting the WOFF2 files is recommended; see Load Fonts below.
Load Fonts
Farn uses Fraunces (display/headings), Instrument Sans (body/UI), and JetBrains Mono (code). Download the four WOFF2 files from the Farn repo, place them at /fonts/ in your public root, and declare them with @font-face:
/* Fraunces — variable (wt 300–900), upright */
@font-face {
font-family: 'Fraunces';
font-style: normal;
font-weight: 300 900;
font-display: swap;
src: url('/fonts/fraunces-latin-normal.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Fraunces';
font-style: italic;
font-weight: 300 900;
font-display: swap;
src: url('/fonts/fraunces-latin-italic.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* Instrument Sans — variable (wt 400–600), upright */
@font-face {
font-family: 'Instrument Sans';
font-style: normal;
font-weight: 400 600;
font-display: swap;
src: url('/fonts/instrument-sans-latin.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* JetBrains Mono — variable (wt 400–500), upright */
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 400 500;
font-display: swap;
src: url('/fonts/jetbrains-mono-latin.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}Self-hosting serves fonts from your own domain — no IP addresses are sent to third-party servers, which satisfies GDPR obligations without a consent banner. If you prefer the Google Fonts CDN, add a consent management platform before enabling it.
Prevent Flash of Wrong Theme (FOWT)
Add this inline script in <head> before any CSS loads. It reads the stored preference and sets data-theme on <html> before the browser paints.
<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>Dark / Light Mode
Theme mode is controlled via a data-theme attribute on <html>:
<html data-theme="light"> … </html>
<html data-theme="dark"> … </html>Toggle it with JavaScript and persist the choice:
function toggleTheme() {
const current = document.documentElement.getAttribute('data-theme');
const next = current === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', next);
localStorage.setItem('farn-theme', next);
}Default (no attribute) is light mode. System preference is respected when no manual choice is stored.
Surfaces and Theme Overrides
Use data-surface to set a depth level on any element. All three surfaces are relative — they adapt automatically when the page theme switches:
<!-- depth level: adapts to current theme -->
<div data-surface="base">…</div> <!-- page-level bg -->
<div data-surface="layer">…</div> <!-- cards, panels -->
<div data-surface="overlay">…</div> <!-- modals, dropdowns -->
<!-- forced theme on any element: combine with data-surface -->
<section data-theme="dark" data-surface="layer">
<!-- dark panel regardless of page theme -->
</section>All children inherit the re-scoped tokens automatically — no conditional logic needed.
Using Tokens
Reference semantic tokens in your component CSS — never raw palette tokens:
.btn-primary {
background: var(--color-accent); /* always Fern */
color: var(--color-accent-text); /* always Parchment */
border-radius: var(--radius-md); /* 6px */
padding: 0 var(--space-md);
font-family: var(--font-body);
font-weight: 600;
}
.card {
background: var(--card-bg);
border-radius: var(--card-radius);
padding: var(--card-padding);
}Customising components (Tier-3 tokens)
Each shipped component exposes Tier-3 tokens — one level above semantic tokens — so you can retheme a single component without touching the rest of the system. Override them on :root for global changes, or scope them to any element:
/* Rebrand the primary button globally */
:root {
--btn-p-bg: #your-brand;
--btn-p-text: white;
}
/* Different brand colour inside a specific section */
.marketing-hero {
--btn-p-bg: #alternate-brand;
}Each component's full token table is listed in the reference section of its group page (Actions, Forms, Layout, etc.).
Fraunces Headings
font-variation-settings: 'opsz' is mandatory on every Fraunces heading — the font renders incorrectly without it:
h1 { font-family: var(--font-display); font-variation-settings: 'opsz' 72; }
h2 { font-family: var(--font-display); font-variation-settings: 'opsz' 24; }
h3 { font-family: var(--font-display); font-variation-settings: 'opsz' 20; }Minimal Example
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/jabopiti/farn-theme@0.7.0/dist/farn.css">
<link rel="stylesheet" href="/styles/fonts.css"><!-- self-hosted @font-face, see Load Fonts -->
<script>
(function(){
const s = localStorage.getItem('farn-theme');
const p = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', s ?? p);
})();
</script>
<style>
body {
font-family: var(--font-body);
background: var(--color-bg);
color: var(--color-text);
padding: var(--space-xl);
}
h1 {
font-family: var(--font-display);
font-variation-settings: 'opsz' 72;
font-weight: 800;
}
</style>
</head>
<body>
<h1>Hello, Farn</h1>
<p>Token-first design system.</p>
</body>
</html>Need a starting point? Browse Templates — copy-paste page sections assembled into complete layouts, all built with Farn tokens.