FairGarden Design

Index Rows

Ruled listing rows for many same-kind items: articles, events, reports, ranked topics and related pages. The default listing on reading pages.

import {
  IndexRow,
  IndexRows,
  IndexRowsList,
  IndexRowTitle,
  IndexRowTitleLink,
} from '@fairgarden-private/design/components/IndexRows'

Each row has one title Link whose hit area covers the row; the row draws the focus ring inside its rules. Rules run from the text column to the module edge, never under the key column, and rows are never boxed. Numbers and dates use tabular figures. Use cards instead only when images drive the choice.

Ranked, related and article

Ranked, related and article

kind is required. ranked puts a muted rank numeral (no period) in a narrow column and expects a count or a stated ranking basis; it renders an ordered list. related puts a 72 px thumbnail beside each title. article sets serif titles over a caps meta line and an optional dek; from 1024 px of container the meta moves to a right-aligned column. Mark the current row with aria-current on its link, which adds the 3 px bar. index on the link adds the visited ✓ for long indexes.

Most read topics

  1. 1
  2. 2
  3. 3
IndexRowsKinds.tsx
import {
  IndexRow,
  IndexRowCount,
  IndexRowDek,
  IndexRowMeta,
  IndexRows,
  IndexRowsFooter,
  IndexRowsHeader,
  IndexRowsList,
  IndexRowTitle,
  IndexRowTitleLink,
} from '@fairgarden-private/design/components/IndexRows'
import { Link } from '@fairgarden-private/design/components/Link'
import { thumb } from './thumb'
import styles from './kinds.module.css'

const topics = [
  { label: 'Birding', count: 29 },
  { label: 'Trail work', count: 21 },
  { label: 'Native plants', count: 17 },
]

/** Ranked (with counts), related (with thumbnails) and article rows. */
export function IndexRowsKinds() {
  return (
    <div className={styles.stack}>
      <IndexRows kind="ranked">
        <IndexRowsHeader>
          <h2 className={styles.heading}>Most read topics</h2>
        </IndexRowsHeader>
        <IndexRowsList>
          {topics.map((topic, index) => (
            <IndexRow key={topic.label} rank={index + 1}>
              <IndexRowTitle>
                <IndexRowTitleLink href="#kinds" aria-current={index === 1 ? 'page' : undefined}>
                  {topic.label}
                </IndexRowTitleLink>
                <IndexRowCount>({topic.count})</IndexRowCount>
              </IndexRowTitle>
            </IndexRow>
          ))}
        </IndexRowsList>
        <IndexRowsFooter>
          <Link kind="standalone" href="#kinds">
            See all topics
          </Link>
        </IndexRowsFooter>
      </IndexRows>

      <IndexRows kind="related">
        <IndexRowsHeader>
          <h2 className={styles.heading}>Related</h2>
        </IndexRowsHeader>
        <IndexRowsList>
          {['How owls hear', 'Nest boxes that work'].map((title) => (
            <IndexRow key={title} thumb={<img src={thumb} alt="" />}>
              <IndexRowTitle>
                <IndexRowTitleLink href="#kinds">{title}</IndexRowTitleLink>
              </IndexRowTitle>
            </IndexRow>
          ))}
        </IndexRowsList>
      </IndexRows>

      <IndexRows kind="article">
        <IndexRowsList>
          <IndexRow>
            <IndexRowTitle>
              <IndexRowTitleLink href="#kinds" index>
                The long return of the river otter
              </IndexRowTitleLink>
            </IndexRowTitle>
            <IndexRowMeta>Field notes · 12 Sep 2026</IndexRowMeta>
            <IndexRowDek>Twenty years after the last sighting, tracks on the sandbar.</IndexRowDek>
          </IndexRow>
          <IndexRow>
            <IndexRowTitle>
              <IndexRowTitleLink href="#kinds" index>
                Why we leave the dead trees standing
              </IndexRowTitleLink>
            </IndexRowTitle>
            <IndexRowMeta>Stewardship · 3 Sep 2026</IndexRowMeta>
          </IndexRow>
        </IndexRowsList>
      </IndexRows>
    </div>
  )
}

Event index

Event index

Event rows carry a mono date key, --role-rule rules and, from 768 px of container, a column rule between the date and the title. IndexRowDisclosure makes the title the trigger of a Collapsible: collapsed titles are muted, open ones grow and take the full ink, and the LTA glyph turns inward. Past events say "Past" in words; they are never dimmed. Rows print expanded with the date column at the left.

  • In person · 7 p.m.

  • In person · 10 a.m.

  • Virtual · Past

IndexRowsEvent.tsx
import { Button } from '@fairgarden-private/design/components/Button'
import {
  IndexRow,
  IndexRowDisclosure,
  IndexRows,
  IndexRowsList,
} from '@fairgarden-private/design/components/IndexRows'
import styles from './event.module.css'

const events = [
  { date: '2026-10-03', key: '03 / 10', title: 'Night walk at Hemlock Ravine', meta: 'In person · 7 p.m.' },
  { date: '2026-10-11', key: '11 / 10', title: 'Seed swap', meta: 'In person · 10 a.m.' },
  { date: '2026-09-12', key: '12 / 09', title: 'Stream ecology talk', meta: 'Virtual · Past' },
]

/** Event rows expand in place; collapsed titles are muted, open titles grow and darken. */
export function IndexRowsEvent() {
  return (
    <IndexRows kind="event">
      <IndexRowsList>
        {events.map((event) => (
          <IndexRow key={event.title} date={<time dateTime={event.date}>{event.key}</time>}>
            <IndexRowDisclosure title={event.title} meta={event.meta}>
              <p>Meet at the north kiosk. Bring a red light; we walk slowly and quietly.</p>
              <dl className={styles.pairs}>
                <dt>Host</dt>
                <dd>Ana Díaz</dd>
                <dt>Entrance</dt>
                <dd>North lot</dd>
              </dl>
              <Button size="sm">Register</Button>
            </IndexRowDisclosure>
          </IndexRow>
        ))}
      </IndexRowsList>
    </IndexRows>
  )
}

API Reference

IndexRows

The index module: an optional IndexRowsHeader (the §11.8 eyebrow and hairline, plus any Tabs), the IndexRowsList of IndexRows and an optional IndexRowsFooter (“See all ›"). Rows are separated by rules, never boxed; numbers and dates use tabular figures.

PropTypeDescription
kind
IndexRowsKind

Required. ranked: a muted rank numeral column, labels with counts, hairline rules. related: a thumbnail left of each title, hairline rules. event: a mono date key, --role-rule rules and a column rule; rows may expand. article: serif titles, a caps meta line, optional deks, hairline dividers.

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: titles, keys, rules, bars and glyphs. Never defaulted; omitted, it inherits the scope [D133].

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: the title links’ underline and the footer chevron (--role-accent, --role-glyph). Never defaulted.

IndexRowsHeader, IndexRowsList and IndexRowsFooter

The module header slot: the §11.8 section header (eyebrow + --role-hairline rule) and, where needed, “Upcoming / Past” Tabs (§9.5). Stays with the first row in print.

The rows: an ol for ranked lists, whose order means something; a ul otherwise.

The module footer: a standalone Link (“See all ›", type-label caps with its --role-glyph chevron). Prints “Full list (short URL)” for lists of more than 10 rows (§7.6).

IndexRow

One row: an optional key column (rank, date or thumb) and the text column, which holds IndexRowTitle (or an event’s IndexRowDisclosure), IndexRowMeta and IndexRowDek. The row rule runs from the text column to the module edge, never under the key column.

PropTypeDescription
date
React.ReactNode | undefined

Event rows: the date key, e.g. <time dateTime="2026-07-01">01 / 07</time>; type-data. Above the title below 768 px of container, a left column from 768.

rank
React.ReactNode | undefined

Ranked rows: the rank numeral, with no period (“1″, “2″ …); --role-muted, tabular.

thumb
React.ReactNode | undefined

Related rows: a thumbnail img (empty alt), --ds-size-thumb square, dropped in print.

The row’s title: type-itemhead for article and event rows, type-body-ui at --font-weight-6 for ranked and related rows. Wrap an IndexRowTitleLink inside, and follow it with an IndexRowCount in ranked lists.

The row’s one link, inside IndexRowTitle: a §9.3 title Link whose hit area stretches over the row. Pass aria-current="page" (or "true" for the active filter row) to mark the current row. index adds the visited ✓ for long indexes.

PropTypeDescription
external
boolean | null | undefined

Adds the arrow-open mark and “(external site)” for assistive technology. Default false.

index
boolean | null | undefined

Adds the screen-only visited ✓ used in long indexes, such as reference and archive lists. Default false [D174, D175].

list
boolean | null | undefined

With kind="nav": a list link (nav-panel, footer, drawer and breadcrumb lists), whose hover is --role-link-hover color only, plus the --ds-stroke-1-5 --role-accent underline where that ink is --primary12. Without it, nav is bare navigation text (bar and utility items, page numbers, toolbar links), whose hover is the --border-size-2 --role-accent underline (§9.3) [D181]. Default false.

muted
boolean | null | undefined

Rests in --role-muted instead of --primary12, as a breadcrumb’s ancestors. Hover takes --role-link-hover only (the underline is added where that ink is --primary12); with kind="nav" it replaces the bar item’s bare-text underline (§9.3, §9.8) [D181]. Default false.

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: the link text and focus ring. Never defaulted; omitted, it inherits the scope [D133].

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: the accent underline. Never defaulted; omitted, it inherits the scope.

className
string | undefined

Extra class names, added after the module’s own.

The ranked label’s count, “(29)": type-body-ui in --role-muted, after the title.

IndexRowMeta and IndexRowDek

The meta line in --role-muted: type-label caps for article rows, type-caption otherwise. Past rows say “Past” here; never dim them. From 1024 px of container an article row’s meta moves to a right-aligned column.

An optional one- or two-line dek under an article title: type-body-ui in --primary12.

IndexRowDisclosure

An expandable event row (§10.13): the title is the trigger, inside an h3, with the LTA disclosure glyph; the details (body, / “Entrance” pairs, a sm Button) sit in the panel. Collapsed titles are --role-muted and grow to type-h2 in --primary12 when open. Rows print expanded, glyphs hidden.

PropTypeDescription
meta
React.ReactNode | undefined

An optional meta line under the title (format, place; “Past”), shown in both states.

panelProps
Omit<CollapsiblePanelProps, 'children'> | undefined

Props for the details panel.

title
React.ReactNode | undefined

The event title, the trigger’s label: --role-muted collapsed, --primary12 open.

Additional types

IndexRowCountProps

Props for IndexRowCount: span props and render.

type IndexRowCountProps = useRender.ComponentProps<'span'>
IndexRowDekProps

Props for IndexRowDek: paragraph props and render.

type IndexRowDekProps = useRender.ComponentProps<'p'>
IndexRowDisclosureProps

Props for IndexRowDisclosure: Base UI Collapsible Root props plus the title, meta and panel props.

type IndexRowDisclosureProps = {
  /** The event title, the trigger's label: `--role-muted` collapsed, `--primary12` open. */
  title: React.ReactNode;
  /** An optional meta line under the title (format, place; "Past"), shown in both states. */
  meta?: React.ReactNode;
  /** Props for the details panel. */
  panelProps?: Omit<CollapsiblePanelProps, 'children'>;
}
IndexRowMetaProps

Props for IndexRowMeta: paragraph props and render.

type IndexRowMetaProps = useRender.ComponentProps<'p'>
IndexRowProps

Props for IndexRow: list-item props, render and the key column content.

type IndexRowProps = React.ClassAttributes<HTMLLIElement> &
  React.LiHTMLAttributes<HTMLLIElement> &
  __type & { rank?: React.ReactNode; date?: React.ReactNode; thumb?: React.ReactNode }
indexRows
IndexRowsFooterProps

Props for IndexRowsFooter: div props and render.

type IndexRowsFooterProps = useRender.ComponentProps<'div'>
IndexRowsHeaderProps

Props for IndexRowsHeader: header props and render.

type IndexRowsHeaderProps = useRender.ComponentProps<'header'>
IndexRowsKind

The four listing builds (§12.11).

type IndexRowsKind = 'ranked' | 'related' | 'event' | 'article'
IndexRowsListProps

Props for IndexRowsList: list props and render.

type IndexRowsListProps = useRender.ComponentProps<'ul'>
IndexRowsProps

Props for IndexRows: section props, render, the kind and the color axes.

type IndexRowsProps = React.ClassAttributes<HTMLElement> &
  React.HTMLAttributes<HTMLElement> &
  __type & {
    kind: IndexRowsKind;
    primary?:
      | 'ruby'
      | 'olive'
      | 'sage'
      | 'slate'
      | 'sand'
      | 'gray'
      | 'mauve'
      | 'brown'
      | 'bronze'
      | 'gold'
      | 'red'
      | 'crimson'
      | 'tomato'
      | 'pink'
      | 'plum'
      | 'indigo'
      | 'iris'
      | 'violet'
      | 'purple'
      | null;
    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;
  }
IndexRowTitleLinkProps

Props for IndexRowTitleLink: Link props except kind, which is always title.

type IndexRowTitleLinkProps = {
  /** Extra class names, added after the module's own. */
  className?: string;
  /**
   * Primary Radix scale: the link text and focus ring. Never defaulted;
   * omitted, it inherits the scope [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: the accent underline. Never defaulted; omitted,
   * it inherits the scope.
   */
  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;
  /**
   * With `kind="nav"`: a list link (nav-panel, footer, drawer and breadcrumb
   * lists), whose hover is `--role-link-hover` color only, plus the
   * `--ds-stroke-1-5` `--role-accent` underline where that ink is
   * `--primary12`. Without it, `nav` is bare navigation text (bar and
   * utility items, page numbers, toolbar links), whose hover is the
   * `--border-size-2` `--role-accent` underline (§9.3) [D181]. Default
   * `false`.
   */
  list?: boolean | null;
  /**
   * Adds the screen-only visited ✓ used in long indexes, such as reference
   * and archive lists. Default `false` [D174, D175].
   */
  index?: boolean | null;
  /**
   * Adds the arrow-open mark and "(external site)" for assistive technology.
   * Default `false`.
   */
  external?: boolean | null;
  /**
   * Rests in `--role-muted` instead of `--primary12`, as a breadcrumb's
   * ancestors. Hover takes `--role-link-hover` only (the underline is added
   * where that ink is `--primary12`); with `kind="nav"` it replaces the bar
   * item's bare-text underline (§9.3, §9.8) [D181]. Default `false`.
   */
  muted?: boolean | null;
}
IndexRowTitleProps

Props for IndexRowTitle: heading props and render (default <h3>).

type IndexRowTitleProps = useRender.ComponentProps<'h3'>

Specification: DESIGN-SYSTEM.md §12.11 (ruled index rows) and §5.10.2 (module reflow).