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

Tip: 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-theme

Then 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:

  1. colors.css — 17 palette tokens across four palettes
  2. typography.css — font-family tokens and scale reference
  3. spacing.css — space scale, layout widths, border radius, z-index
  4. motion.css — duration scale and easing tokens
  5. dark-light.css — semantic token layer with dark/light switching
  6. components.css — Tier-3 component tokens (--btn-*, --card-*, --input-*, etc.)
  7. 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.