Components
Farn is a token system, not a component library. This reference specifies what components should look like and how they should behave — using which tokens, with which states, following which accessibility rules. Implementation lives in your project.
All components use semantic tokens only — never raw palette tokens. The surface declaration handles dark/light adaptation automatically.
Cards
Flat inset style — background fill, no border at rest. Cards are entry points, not content containers. Body text: 2–3 lines maximum.
Variants
| Variant | Background | Use |
|---|---|---|
| Default | --bs1-linen / --kn1-iron (dark) | Article lists, feature grids, project indexes |
| Highlight — light | --kn0-void | Featured content on light backgrounds |
| Highlight — dark | --kn1-iron + 1px --kn2-slate border | Featured content on dark backgrounds |
Grid & Sizing
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: var(--space-md); /* 24px */
}
.card {
background: var(--color-bg-elevated);
border-radius: var(--radius-lg); /* 8px */
padding: 20px;
}
/* Clickable card */
.card:hover { background: var(--color-bg-sunken); }
.card:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 3px;
} Buttons
Five variants, three sizes. Buttons adapt to surface context via semantic tokens — the surface declaration handles dark/light automatically, no conditional logic needed.
Variants
| Variant | Light bg | Dark bg | Use |
|---|---|---|---|
| Primary | --fr1-fern, Parchment text | --fr0-sage, Parchment text | The single strongest action per context |
| Secondary | --bs0-sand, Void text | --kn2-slate, Parchment text | Second most important action |
| Ghost | Transparent, Iron border | Transparent, Slate border | Low-priority — cancel, back, dismiss |
| Destructive | --bl0-ember, Parchment text (both modes) | Irreversible actions only | |
| Text link | --fr1-fern text, underline | --fr0-sage text, underline | Tertiary actions in prose or UI text |
Sizes
| Size | Height | H. Padding | Font |
|---|---|---|---|
| Small | 32px | 12px | 12px, weight 600 |
| Default | 40px | 16px | 13px, weight 600 |
| Large | 48px | 20px | 14px, weight 600 |
All sizes: 6px border radius (--radius-md). One primary button per action context — two primaries compete and neither wins.
States
| State | Treatment |
|---|---|
| Hover | Background shift — transition: background-color 120ms ease-out |
| Focus | outline: 2px solid var(--color-accent), outline-offset: 3px |
| Active | transform: scale(0.98) |
| Disabled | 40% opacity, cursor: not-allowed |
Forms & Inputs
Filled style, comfortable density (48px height). Chrome only appears when needed — borders emerge on focus and error, not by default.
Input States (light surface)
| State | Background | Border |
|---|---|---|
| Rest | --bs1-linen | none |
| Hover | --bs0-sand | none |
| Focus | --bs0-sand | 1.5px --fr1-fern + Fern glow |
| Error | --bs2-parchment | 1.5px --bl0-ember |
| Disabled | --bs2-parchment | none, 40% opacity |
input:focus {
outline: none;
border: 1.5px solid var(--color-accent);
box-shadow: 0 0 0 3px rgba(62, 122, 98, 0.15); /* Fern glow */
} Validation
Validate on blur — not on every keystroke. Show errors inline below the field. On submit, scroll to and focus the first errored field. Error messages are specific: “Enter a valid email address” not “Invalid input”.
Navigation
Main Nav
Horizontal top bar, sticky. Hides on scroll down, reappears on scroll up. Transparent over hero, fills on scroll. Maximum 4–6 links.
| Property | Value |
|---|---|
| Height | 64px |
| Nav link font | --font-body, 14px, weight 500 |
| Active link | --color-accent, weight 600 |
| Active link treatment | Colour + weight shift only — no background pill |
In-Page Tab Navigation
Horizontal tabs with underline active indicator. Switches views within a single screen — not routing between pages. Active indicator slides between tabs rather than appearing/disappearing.
<div role="tablist" aria-label="Content sections">
<button role="tab" aria-selected="true"
aria-controls="panel-overview" id="tab-overview">Overview</button>
<button role="tab" aria-selected="false"
aria-controls="panel-details" id="tab-details" tabindex="-1">Details</button>
</div>
<div role="tabpanel" id="panel-overview" aria-labelledby="tab-overview"> … </div>
<div role="tabpanel" id="panel-details" aria-labelledby="tab-details" hidden> … </div> Badges & Tags
Subtle tinted — low-opacity Bloom palette fills, 4px border radius. Informational only, never interactive except for dismissible tags.
| Status | Colour |
|---|---|
| Published / Active / Success | Moss — --bl3-moss |
| Draft / Pending / Warning | Grain — --bl2-grain |
| Archived / Error / Removed | Ember — --bl0-ember |
| Beta / Special | Heather — --bl4-heather |
| Primary category | Forest — --fr1-fern |
| Neutral / Uncategorised | Sand — --bs0-sand |
.badge {
display: inline-flex;
align-items: center;
height: 22px;
padding: 3px 8px;
border-radius: var(--radius-sm); /* 4px */
font-size: 11px;
font-weight: 600;
letter-spacing: 0.04em;
}
.badge-moss {
background: rgba(86, 122, 55, 0.12);
color: #3A5523;
}
[data-theme="dark"] .badge-moss {
background: rgba(86, 122, 55, 0.15);
color: #7AAD52;
} Focus Styles
A single consistent focus ring across all interactive elements — defined once in base.css, applied everywhere via :focus-visible. Never remove focus styles.
:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 3px;
border-radius: inherit;
}
:focus:not(:focus-visible) {
outline: none;
} focus-visible ensures the ring only appears for keyboard navigation — not on mouse click. border-radius: inherit follows the element's shape automatically.
Dark / Light Sections
Full-width page sections with explicit surface declarations. All child components inherit the correct tokens automatically — no extra markup needed.
<section data-surface="light"> … </section>
<section data-surface="dark"> … </section>
<section data-surface="tinted"> … </section> | Surface | Background | Use |
|---|---|---|
light | --bs2-parchment | Default. Most content sections. |
dark | --kn0-void | Hero, feature callouts, closing CTAs. |
tinted | --bs1-linen | Subtle variation within a light page. |
Sections are flush — no gap between them. The background colour change is the visual separator. Page rhythm example: dark hero → light features → tinted aside → dark CTA.