FairGarden Design

Hero

The page's one opening statement and its single h1. A plain title block counts. Never text on a photo, never sized to the viewport, never animated on load.

import { Hero, HeroLockup } from '@fairgarden-private/design/components/Hero'

The hero is a band: a page ground (by default the one it sits on), or preset="night" as the media hero. Pass wayfinding (a breadcrumb, eyebrow or badge), title, lede and actions: one solid pill at size="lg" and at most its outline twin. Below 360 px the pair stacks, primary first. Render it inside main.

The exit seam. When something crosses the hero's bottom edge (the stacked photo, the entry cards, the rail's trail), or when the night hero ends on the hill or the fringe, the hero also renders the top of the next band, on next. That keeps the parts that land there in its colors. Open your next band on the same page ground.

Editorial and the media hero

Editorial and the media hero

A is type only: wayfinding, the display title (about 20ch, 2–3 lines), the lede and the actions, in the reading span. landing allows the larger display size from 1024 px. With a photo it becomes the centred media hero on the night band, with the captioned photo below the text. From 1024 px a photo hero may carry up to three entries: compact cards with a square image, a caps category and one link (each a Card kind="entry"), each 4 of 12 columns, straddling the bottom edge. Below 1024 px they stack under the hero. edge="hill" ends the night hero on the hill in light mode.

Annual report

Land for good, for everyone

Ten years of protected acres, open trails and neighbors who kept them.

The prairie, one acre at a time

A reference page's media hero: the photo figure under centred text.

Tallgrass prairie at dawn under a low sun
Nachusa Grasslands, Illinois. Photo: A. Rivera

The white band below opens with its section space; the cards stack here below 1024 px.

HeroEditorial.tsx
import { Button } from '@fairgarden-private/design/components/Button'
import { Ground } from '@fairgarden-private/design/components/Ground'
import { Hero } from '@fairgarden-private/design/components/Hero'
import { photo, square } from '../photo'
import styles from './editorial.module.css'

/**
 * A on paper, then A as the media hero on the night band with three entry
 * cards across its edge into the white band below.
 */
export function HeroEditorial() {
  return (
    <div className={styles.page}>
      <Hero
        wayfinding={<p className={styles.eyebrow}>Annual report</p>}
        title={
          <>
            Land for good, <em>for everyone</em>
          </>
        }
        lede="Ten years of protected acres, open trails and neighbors who kept them."
        actions={
          <>
            <Button variant="solid" size="lg">
              Read the Report
            </Button>
            <Button size="lg">Download PDF</Button>
          </>
        }
      />
      <Hero
        preset="night"
        next="white"
        title="The prairie, one acre at a time"
        lede="A reference page's media hero: the photo figure under centred text."
        photo={<img src={photo} alt="Tallgrass prairie at dawn under a low sun" />}
        caption="Nachusa Grasslands, Illinois. Photo: A. Rivera"
        entries={[
          {
            href: '#editorial',
            category: 'Program',
            title: 'Protect your land',
            image: <img src={square} alt="" />,
          },
          {
            href: '#editorial',
            category: 'Events',
            title: 'Rally 2026',
            image: <img src={square} alt="" />,
          },
          {
            href: '#editorial',
            category: 'Resources',
            title: 'The learning center',
            image: <img src={square} alt="" />,
          },
        ]}
      />
      <Ground kind="band" preset="white" className={styles.band}>
        <div className={styles.container}>
          <p className={styles.text}>
            The white band below opens with its section space; the cards stack here below 1024 px.
          </p>
        </div>
      </Ground>
    </div>
  )
}

Split photo

Split photo

C sets a flat text band beside the photo from 1024 px: text in 5 columns, the photo in 7, top-aligned, reaching the container edge. Below 1024 px the photo follows the text, inset 24 px. The caption sits right-flush below it: the description, then the credit. photoAction puts an icon-only media Button on the photo's corner, and plate an opaque plate over its lower edge. rail pins "Explore the land" to the hero's bottom corner from 768 px, reading upward, flush with the viewport edge. Its dashed trail crosses into the next band from 1024 px. Below 768 px the rail is an inline link under the lede.

Keeping farms in farming

Easements that let a family keep working the land, and keep it open for good.

A family walking a hayfield at the edge of a wood
The Okafor farm, Wisconsin. Photo: J. Lee

The trail lands in this band, in its accent.

HeroSplit.tsx
import { Breadcrumb } from '@fairgarden-private/design/components/Breadcrumb'
import { Button } from '@fairgarden-private/design/components/Button'
import { Ground } from '@fairgarden-private/design/components/Ground'
import { Hero } from '@fairgarden-private/design/components/Hero'
import { photo } from '../photo'
import styles from './split.module.css'

/**
 * C on the night band: the text beside the photo from 1024 px, a media
 * Button on the photo, and the explore rail with its trail into paper.
 */
export function HeroSplit() {
  return (
    <div className={styles.page}>
      <Hero
        kind="split"
        preset="night"
        next="paper"
        wayfinding={<Breadcrumb items={[{ label: 'Programs', href: '#split' }]} current="Farmland" />}
        title="Keeping farms in farming"
        lede="Easements that let a family keep working the land, and keep it open for good."
        actions={
          <Button variant="solid" size="lg">
            Talk to Us
          </Button>
        }
        photo={<img src={photo} alt="A family walking a hayfield at the edge of a wood" />}
        caption="The Okafor farm, Wisconsin. Photo: J. Lee"
        photoAction={
          <Button iconOnly onMedia icon="zoom_in">
            Enlarge the Photo
          </Button>
        }
        rail={{ href: '#split', label: 'Explore the land' }}
      />
      <Ground kind="band" preset="paper" className={styles.band}>
        <div className={styles.container}>
          <p className={styles.text}>The trail lands in this band, in its accent.</p>
        </div>
      </Ground>
    </div>
  )
}

C, stacked

C, stacked

For editorial and reference pages, stacked keeps the reading axis centred. From 1024 px the text band sits in the reading span, then the photo in the same span crosses the night band's edge at its middle. The hill passes behind it, and the caption sits right-flush below it on the page ground. On a dark page the hill drops for the straight seam and the photo still straddles it.

The birds came back to Boone Creek

Five years after the dams came out, the creek's thrushes are nesting again.

A wooded creek bend in early summer
Boone Creek, Kentucky, in June. Photo: M. Chen

The article begins here, on the page ground.

HeroStacked.tsx
import { Breadcrumb } from '@fairgarden-private/design/components/Breadcrumb'
import { Ground } from '@fairgarden-private/design/components/Ground'
import { Hero } from '@fairgarden-private/design/components/Hero'
import { photo } from '../photo'
import styles from './stacked.module.css'

/**
 * "C, stacked": the text band in the reading span, then the photo crossing
 * the night band's edge, the hill behind it and the caption on paper.
 */
export function HeroStacked() {
  return (
    <div className={styles.page}>
      <Hero
        kind="split"
        stacked
        preset="night"
        edge="hill"
        next="paper"
        wayfinding={
          <Breadcrumb items={[{ label: 'News', href: '#stacked' }]} current="Stories" />
        }
        title="The birds came back to Boone Creek"
        lede="Five years after the dams came out, the creek's thrushes are nesting again."
        photo={<img src={photo} alt="A wooded creek bend in early summer" />}
        caption="Boone Creek, Kentucky, in June. Photo: M. Chen"
      />
      <Ground kind="band" preset="paper" className={styles.band}>
        <div className={styles.container}>
          <p className={styles.text}>The article begins here, on the page ground.</p>
        </div>
      </Ground>
    </div>
  )
}

Illustrated

Illustrated

B pairs the text with a sticker drawing and its halo. At base the drawing sits on the headline, centred, at 152 px. From 1024 px the text takes 7 columns and the drawing, at 312 px, takes 5. It is the only hero that may carry a Display Lockup: pass <HeroLockup caps="" accent="" /> as the title. field sets the lockup and drawing in the page's one leaf or amber campaign field.

Grown right here

Burgers from farms within a hundred miles, cooked on the corner since 2009.

The rail’s trail crosses into this band.

HeroIllustrated.tsx
import { Button } from '@fairgarden-private/design/components/Button'
import { Ground } from '@fairgarden-private/design/components/Ground'
import { Hero, HeroLockup } from '@fairgarden-private/design/components/Hero'
import styles from './illustrated.module.css'

/** B on paper: the Display Lockup and a sticker in the page's leaf campaign field. */
export function HeroIllustrated() {
  return (
    <div className={styles.page}>
      <Hero
        kind="illustrated"
        field="leaf"
        next="paper"
        title={<HeroLockup caps="Grown right" accent="here" />}
        lede="Burgers from farms within a hundred miles, cooked on the corner since 2009."
        actions={
          <Button variant="solid" size="lg">
            Order Now
          </Button>
        }
        drawing={
          <svg viewBox="0 0 312 312" role="img" aria-label="A sprouting seed">
            <circle cx="156" cy="170" r="104" fill="var(--role-halo)" />
            <path
              d="M156 250v-86m0 0c0-40 28-64 64-64-4 40-28 64-64 64Zm0 0c0-34-24-56-58-56 4 34 26 56 58 56Z"
              fill="none"
              stroke="var(--primary12)"
              strokeWidth="6"
              strokeLinecap="round"
              strokeLinejoin="round"
            />
            <path
              d="M96 250h120"
              stroke="var(--primary12)"
              strokeWidth="6"
              strokeLinecap="round"
            />
          </svg>
        }
        rail={{ href: '#illustrated', label: 'Find a kitchen' }}
      />
      <Ground kind="band" preset="paper" className={styles.band}>
        <div className={styles.container}>
          <p className={styles.text}>The rail&rsquo;s trail crosses into this band.</p>
        </div>
      </Ground>
    </div>
  )
}

Technical

Technical

D frames the title, a rule, a mono subtitle and the actions in a double rule. From 1024 px an aside column sits behind a vertical rule. The cells row reflows on its own width: one column below 360 px, 2-up from 360 px, 4-up from 480 px. Any dot grid or graph pattern stays outside the frame.

Respinner

v2.4.0 · MIT · 4 kB

Install

npm i respinner
Size
4 kB
Dependencies
0
Browsers
98%
License
MIT
HeroTechnical.tsx
import { Button } from '@fairgarden-private/design/components/Button'
import { Hero } from '@fairgarden-private/design/components/Hero'
import patterns from '@fairgarden-private/design/utils/pattern.module.css'
import styles from './technical.module.css'

/** D on white: the double frame, the install column and the info cells; the dot grid stays outside the frame. */
export function HeroTechnical() {
  return (
    <Hero
      kind="technical"
      preset="white"
      className={patterns.patternDotgrid}
      title="Respinner"
      lede="v2.4.0 · MIT · 4 kB"
      actions={
        <>
          <Button variant="solid" size="lg">
            Get Started
          </Button>
          <Button size="lg">Read the Docs</Button>
        </>
      }
      aside={
        <div className={styles.install}>
          <p className={styles.label}>Install</p>
          <code className={styles.code}>npm i respinner</code>
        </div>
      }
      cells={[
        { label: 'Size', value: '4 kB' },
        { label: 'Dependencies', value: '0' },
        { label: 'Browsers', value: '98%' },
        { label: 'License', value: 'MIT' },
      ]}
    />
  )
}

API Reference

The page’s one hero, with the single h1. Never text on a photo, never sized to the viewport, never animated on load. A photo hero splits: text on a flat ground, the photo as its own captioned figure.

  • editorial (A): type only on the page ground; with a photo, the centred media hero, on preset="night".
  • illustrated (B): text and a sticker drawing; field sets them in a campaign field; HeroLockup is its Display Lockup.
  • split (C): text beside the photo from --lg-n-above; stacked puts the photo under the text in the reading span, straddling the edge.
  • technical (D): the double-ruled frame, with aside and cells.

Straddles [D185]: entries (up to 3) cross the bottom edge from --lg-n-above and stack under the hero below it; the stacked photo crosses it too, with the hill behind. rail pins the rotated label to the bottom end corner, its trail crossing the seam. The hero then renders its exit seam on next, the band below. Prints as the masthead title.

PropTypeDescription
actions
React.ReactNode | undefined

The action Buttons: one solid pill (size="lg") and at most its outline twin.

aside
React.ReactNode | undefined
caption
React.ReactNode | undefined

A content photo figure’s caption: description, then “Photo: Name”; right-flush below.

cells
HeroCell[] | undefined
drawing
React.ReactNode | undefined
edge
'hill' | 'fringe' | undefined
entries
HeroEntries | undefined

Up to 3 entry cards straddling the hero’s bottom edge from --lg-n-above (with a photo).

field
'leaf' | 'amber' | undefined
kind
| 'editorial'
| 'split'
| 'illustrated'
| 'technical'
| undefined

A: type only, or the media hero’s photo figure (centred, on night).

landing
boolean | undefined

Landing pages: the title may take type-display-xl from --lg-n-above. Default false.

lede
React.ReactNode | undefined

The lede: type-lead in --primary12 (a mono subtitle in technical).

next
| 'paper'
| 'white'
| 'tide'
| 'meadow'
| 'pollen'
| 'apricot'
| 'rose'
| 'heather'
| undefined

The page ground of the band below the hero, which the exit seam takes (the stacked photo’s lower half and caption, the entry cards, the trail and the hill all land on it). Default: the enclosing page ground, else paper. Open the next band on the same preset.

photo
React.ReactNode | undefined

The photo, usually an img with its alt, at 3:2. With a photo the text centres.

photoAction
React.ReactNode | undefined

An icon-only onMedia Button over the photo’s corner (the §9.2 media variant).

plate
React.ReactNode | undefined
preset
| 'paper'
| 'white'
| 'tide'
| 'meadow'
| 'pollen'
| 'apricot'
| 'rose'
| 'heather'
| 'night'
| undefined

The hero’s band: a page ground (default: the enclosing page ground, else paper), or night as the media hero [D177].

primary
| 'ruby'
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| null
| undefined

Primary Radix scale for the hero’s scope. Never defaulted; omitted, the preset’s default [D133].

rail
HeroRail | undefined

The explore rail: an inline link under the lede at base; the rotated corner label from --md-n-above.

renderLink
((href: string) => ReactElement) | undefined

Builds each anchor (rail, entry cards), e.g. (href) => <NextLink href={href} />.

secondary
| 'ruby'
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| 'amber'
| 'blue'
| 'cyan'
| 'grass'
| 'green'
| 'jade'
| 'lime'
| 'mint'
| 'orange'
| 'sky'
| 'teal'
| 'yellow'
| null
| undefined

Secondary Radix scale for the hero’s scope: the title, the trail and the accents.

stacked
boolean | false | undefined
title
React.ReactNode

The h1: type-display, about 20ch, in 2–3 lines. Real text, never on a photo.

titleId
string | undefined

The title’s id.

transactional
boolean | undefined

A transactional hero: the actions carry the page’s single --ds-size-control-xl pill (pass size="xl" to its Button). Default false.

wayfinding
React.ReactNode | undefined

Wayfinding above the title: a §9.8 Breadcrumb, an eyebrow, a pill badge or a topic tag.

The Display Lockup (§3.8) as the illustrated hero’s title: caps words and one accent word, real text in the h1. Pass it as title.

PropTypeDescription
accent
React.ReactNode

The one accent word, in type-lockup-accent.

caps
React.ReactNode

The caps words, in type-lockup-caps.

Additional types

hero
type hero = hero
HeroCell

One technical info cell: a caps label over a mono value.

type HeroCell = { label: React.ReactNode; value: React.ReactNode }
HeroEntries

Up to three entry cards [D185].

type HeroEntries = [] | [HeroEntry] | [HeroEntry, HeroEntry] | [HeroEntry, HeroEntry, HeroEntry]
HeroEntry

One compact entry card [D185], rendered as Card kind="entry": square image, caps category, item-head link.

type HeroEntry = {
  /** The card's one link. */
  href: string;
  /** The category, in `type-label` caps. */
  category: React.ReactNode;
  /** The link text, in `type-itemhead`. */
  title: React.ReactNode;
  /** A square image, usually an `img` with its `alt`. */
  image?: React.ReactNode;
}
HeroKind

The four §11.9 builds: A editorial, B illustrated, C split photo, D technical.

type HeroKind = 'editorial' | 'illustrated' | 'split' | 'technical'<'editorial' | 'illustrated' | 'split' | 'technical' | null | undefined>
HeroLockupProps

Props for HeroLockup.

type HeroLockupProps = {
  /** The caps words, in `type-lockup-caps`. */
  caps: React.ReactNode;
  /** The one accent word, in `type-lockup-accent`. */
  accent: React.ReactNode;
}
HeroProps

Props for Hero: header props, the kind with its parts, the preset (and the night hero’s edge), next, the flags and the color axes.

type HeroProps = (
  | {
      caption?: React.ReactNode;
      photoAction?: React.ReactNode;
      rail?: HeroRail;
      drawing?: undefined;
      field?: undefined;
      aside?: undefined;
      cells?: undefined;
      kind?: 'editorial';
      photo?: React.ReactNode;
      entries?: HeroEntries;
      stacked?: undefined;
      plate?: undefined;
      preset?: 'paper' | 'white' | 'tide' | 'meadow' | 'pollen' | 'apricot' | 'rose' | 'heather';
      edge?: undefined;
    }
  | {
      caption?: React.ReactNode;
      photoAction?: React.ReactNode;
      rail?: HeroRail;
      drawing?: undefined;
      field?: undefined;
      aside?: undefined;
      cells?: undefined;
      kind?: 'editorial';
      photo?: React.ReactNode;
      entries?: HeroEntries;
      stacked?: undefined;
      plate?: undefined;
      preset: 'night';
      edge?: 'hill' | 'fringe';
    }
  | {
      caption?: React.ReactNode;
      photoAction?: React.ReactNode;
      rail?: HeroRail;
      drawing?: undefined;
      field?: undefined;
      aside?: undefined;
      cells?: undefined;
      kind: 'split';
      photo: React.ReactNode;
      stacked: true;
      entries?: undefined;
      plate?: undefined;
      preset?: 'paper' | 'white' | 'tide' | 'meadow' | 'pollen' | 'apricot' | 'rose' | 'heather';
      edge?: undefined;
    }
  | {
      caption?: React.ReactNode;
      photoAction?: React.ReactNode;
      rail?: HeroRail;
      drawing?: undefined;
      field?: undefined;
      aside?: undefined;
      cells?: undefined;
      kind: 'split';
      photo: React.ReactNode;
      stacked: true;
      entries?: undefined;
      plate?: undefined;
      preset: 'night';
      edge?: 'hill' | 'fringe';
    }
  | {
      caption?: React.ReactNode;
      photoAction?: React.ReactNode;
      rail?: HeroRail;
      drawing?: undefined;
      field?: undefined;
      aside?: undefined;
      cells?: undefined;
      kind: 'split';
      photo: React.ReactNode;
      stacked?: false;
      entries?: HeroEntries;
      plate?: React.ReactNode;
      preset?: 'paper' | 'white' | 'tide' | 'meadow' | 'pollen' | 'apricot' | 'rose' | 'heather';
      edge?: undefined;
    }
  | {
      caption?: React.ReactNode;
      photoAction?: React.ReactNode;
      rail?: HeroRail;
      drawing?: undefined;
      field?: undefined;
      aside?: undefined;
      cells?: undefined;
      kind: 'split';
      photo: React.ReactNode;
      stacked?: false;
      entries?: HeroEntries;
      plate?: React.ReactNode;
      preset: 'night';
      edge?: 'hill' | 'fringe';
    }
  | {
      kind: 'illustrated';
      preset?: 'paper' | 'white' | 'tide' | 'meadow' | 'pollen' | 'apricot' | 'rose' | 'heather';
      edge?: undefined;
      drawing?: React.ReactNode;
      field?: 'leaf' | 'amber';
      rail?: HeroRail;
      stacked?: undefined;
      photo?: undefined;
      caption?: undefined;
      photoAction?: undefined;
      plate?: undefined;
      entries?: undefined;
      aside?: undefined;
      cells?: undefined;
    }
  | {
      kind: 'technical';
      preset?: 'paper' | 'white' | 'tide' | 'meadow' | 'pollen' | 'apricot' | 'rose' | 'heather';
      edge?: undefined;
      aside?: React.ReactNode;
      cells?: HeroCell[];
      drawing?: React.ReactNode;
      stacked?: undefined;
      photo?: undefined;
      caption?: undefined;
      photoAction?: undefined;
      plate?: undefined;
      entries?: undefined;
      field?: undefined;
      rail?: undefined;
    }
) & {
  /** Landing pages: the title may take `type-display-xl` from `--lg-n-above`. Default `false`. */
  landing?: boolean;
  /**
   * A transactional hero: the actions carry the page's single
   * `--ds-size-control-xl` pill (pass `size="xl"` to its Button). Default `false`.
   */
  transactional?: boolean;
  /** Primary Radix scale for the hero's scope. Never defaulted; omitted, the preset's default [D133]. */
  primary?:
    | 'ruby'
    | 'olive'
    | 'sage'
    | 'slate'
    | 'sand'
    | 'gray'
    | 'mauve'
    | 'brown'
    | 'bronze'
    | 'gold'
    | 'red'
    | 'crimson'
    | 'tomato'
    | 'pink'
    | 'plum'
    | 'indigo'
    | 'iris'
    | 'violet'
    | 'purple'
    | null;
  /** Secondary Radix scale for the hero's scope: the title, the trail and the accents. */
  secondary?:
    | 'ruby'
    | 'olive'
    | 'sage'
    | 'slate'
    | 'sand'
    | 'gray'
    | 'mauve'
    | 'brown'
    | 'bronze'
    | 'gold'
    | 'red'
    | 'crimson'
    | 'tomato'
    | 'pink'
    | 'plum'
    | 'indigo'
    | 'iris'
    | 'violet'
    | 'purple'
    | 'amber'
    | 'blue'
    | 'cyan'
    | 'grass'
    | 'green'
    | 'jade'
    | 'lime'
    | 'mint'
    | 'orange'
    | 'sky'
    | 'teal'
    | 'yellow'
    | null;
  /** Wayfinding above the title: a §9.8 Breadcrumb, an eyebrow, a pill badge or a topic tag. */
  wayfinding?: React.ReactNode;
  /** The `h1`: `type-display`, about 20ch, in 2–3 lines. Real text, never on a photo. */
  title: React.ReactNode;
  /** The title's `id`. */
  titleId?: string;
  /** The lede: `type-lead` in `--primary12` (a mono subtitle in `technical`). */
  lede?: React.ReactNode;
  /** The action Buttons: one `solid` pill (`size="lg"`) and at most its outline twin. */
  actions?: React.ReactNode;
  /**
   * The page ground of the band below the hero, which the exit seam takes
   * (the stacked photo's lower half and caption, the entry cards, the
   * trail and the hill all land on it). Default: the enclosing page
   * ground, else `paper`. Open the next band on the same preset.
   */
  next?: 'paper' | 'white' | 'tide' | 'meadow' | 'pollen' | 'apricot' | 'rose' | 'heather';
  /** Builds each anchor (rail, entry cards), e.g. `(href) => <NextLink href={href} />`. */
  renderLink?: (href: string) => ReactElement;
}
HeroRail

The explore rail [D185]: the rotated rail label and its optional compass.

type HeroRail = {
  /** Where the rail goes, usually an in-page anchor. */
  href: string;
  /** The label, e.g. "Explore the land". */
  label: React.ReactNode;
  /** An optional compass: an icon-only §9.2 Button that scrolls with the hero, never a fixed launcher. */
  compass?: React.ReactNode;
}

Specification: DESIGN-SYSTEM.md §11.9 (hero), §5.6.2 (straddles) and D185 (entry cards, the explore rail, "C, stacked").