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

VariantBackgroundUse
Default--bs1-linen / --kn1-iron (dark)Article lists, feature grids, project indexes
Highlight — light--kn0-voidFeatured content on light backgrounds
Highlight — dark--kn1-iron + 1px --kn2-slate borderFeatured 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

VariantLight bgDark bgUse
Primary--fr1-fern, Parchment text--fr0-sage, Parchment textThe single strongest action per context
Secondary--bs0-sand, Void text--kn2-slate, Parchment textSecond most important action
GhostTransparent, Iron borderTransparent, Slate borderLow-priority — cancel, back, dismiss
Destructive--bl0-ember, Parchment text (both modes)Irreversible actions only
Text link--fr1-fern text, underline--fr0-sage text, underlineTertiary actions in prose or UI text

Sizes

SizeHeightH. PaddingFont
Small32px12px12px, weight 600
Default40px16px13px, weight 600
Large48px20px14px, weight 600

All sizes: 6px border radius (--radius-md). One primary button per action context — two primaries compete and neither wins.

States

StateTreatment
HoverBackground shift — transition: background-color 120ms ease-out
Focusoutline: 2px solid var(--color-accent), outline-offset: 3px
Activetransform: scale(0.98)
Disabled40% 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)

StateBackgroundBorder
Rest--bs1-linennone
Hover--bs0-sandnone
Focus--bs0-sand1.5px --fr1-fern + Fern glow
Error--bs2-parchment1.5px --bl0-ember
Disabled--bs2-parchmentnone, 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.

PropertyValue
Height64px
Nav link font--font-body, 14px, weight 500
Active link--color-accent, weight 600
Active link treatmentColour + 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.

StatusColour
Published / Active / SuccessMoss — --bl3-moss
Draft / Pending / WarningGrain — --bl2-grain
Archived / Error / RemovedEmber — --bl0-ember
Beta / SpecialHeather — --bl4-heather
Primary categoryForest — --fr1-fern
Neutral / UncategorisedSand — --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>
SurfaceBackgroundUse
light--bs2-parchmentDefault. Most content sections.
dark--kn0-voidHero, feature callouts, closing CTAs.
tinted--bs1-linenSubtle 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.