FairGarden Design

Pricing

Honest, comparable prices: inline prices, sales, menu figures, plan tiers and wholesale matrices.

import {
  Pricing,
  PricingAmount,
  PricingCurrency,
  PricingPeriod,
  PricingPrice,
} from '@fairgarden-private/design/components/Pricing'

Every price is a data element with a machine-readable value. Show the unit and period next to every price, put fees on the price line, and never mark a sale with red alone: the strike and the word "Save" carry it. Dense module: rules and edges only, no ornament.

Inline, sale and menu prices

Inline, sale and menu prices

kind is required. inline sits in running text (pass render={<span />}) with the figure bold. sale strikes the original price in the muted ink, with a hidden "Was" for assistive technology, then the current price (label="Now") and the saving as a word. menu sets a mono figure, with optional superscript cents, for a leader row. Author the spaces between parts in your locale's order; PricingPeriod adds the thin space itself.

Guided walks from $12per person, children free.

Was $480Now $180Save $300
$450
PricingPrices.tsx
import {
  Pricing,
  PricingAmount,
  PricingCents,
  PricingCurrency,
  PricingPeriod,
  PricingPrice,
  PricingQualifier,
  PricingSave,
  PricingStruck,
} from '@fairgarden-private/design/components/Pricing'
import styles from './prices.module.css'

/** Inline in running text, a sale (struck price, current price, the saving as a word) and a menu figure. */
export function PricingPrices() {
  return (
    <div className={styles.stack}>
      <p className={styles.copy}>
        Guided walks{' '}
        <Pricing kind="inline" render={<span />}>
          <PricingPrice value="12">
            <PricingQualifier>from</PricingQualifier> <PricingCurrency>$</PricingCurrency>
            <PricingAmount>12</PricingAmount>
            <PricingPeriod>per person</PricingPeriod>
          </PricingPrice>
        </Pricing>
        , children free.
      </p>

      <Pricing kind="sale">
        <PricingStruck>
          <PricingPrice value="480">
            <PricingCurrency>$</PricingCurrency>
            <PricingAmount>480</PricingAmount>
          </PricingPrice>
        </PricingStruck>
        <PricingPrice value="180" label="Now">
          <PricingCurrency>$</PricingCurrency>
          <PricingAmount>180</PricingAmount>
        </PricingPrice>
        <PricingSave>Save $300</PricingSave>
      </Pricing>

      <Pricing kind="menu">
        <PricingPrice value="4.50">
          <PricingCurrency>$</PricingCurrency>
          <PricingAmount>4</PricingAmount>
          <PricingCents>50</PricingCents>
        </PricingPrice>
      </Pricing>
    </div>
  )
}

Tiers and matrix

Tiers and matrix

tiers holds a PricingTierList of two to four PricingTiers, each a faced card that opens a light face on every ground. recommended draws a 3 px frame and a "Recommended" badge across the top edge; author that tier first, because tiers stack below 1024 px of container and visual order follows source order. From 1024 px they sit side by side. When the plans feed a form, wrap the list in a RadioGroup and put each Radio in its PricingChoice: a checked Radio steps the edge to 3 px, and a disabled one gives a dotted edge and muted text (say "Unavailable" too). matrix is a table with row rules and a double-hair total rule; each PricingCell repeats its column name when rows become blocks below 360 px of container. Close either with PricingAsOf.

  • Member

    $45/ year

    Free parking and the quarterly newsletter.

    • Free parking
    • Quarterly newsletter
  • Patron

    $500/ year

    Sold out for this season.

    • Everything in Steward
    • Annual preserve tour
    Unavailable

Prices as of 1 Sep 2026 (hemlockravine.org/join)

SpeciesRetailWholesale
PawpawRetail$18Wholesale$11
ServiceberryRetail$14Wholesale$9
Minimum orderRetail1Wholesale25
PricingTiers.tsx
import { Button } from '@fairgarden-private/design/components/Button'
import {
  Pricing,
  PricingAction,
  PricingAmount,
  PricingAsOf,
  PricingCell,
  PricingChoice,
  PricingCurrency,
  PricingFeature,
  PricingFeatures,
  PricingMatrix,
  PricingPeriod,
  PricingPrice,
  PricingSummary,
  PricingTier,
  PricingTierList,
  PricingTierName,
} from '@fairgarden-private/design/components/Pricing'
import { Radio } from '@fairgarden-private/design/components/Radio'
import { RadioGroup } from '@fairgarden-private/design/components/RadioGroup'
import styles from './tiers.module.css'

const tiers = [
  {
    value: 'steward',
    name: 'Steward',
    amount: '120',
    summary: 'For regulars who want to give back.',
    features: ['Everything in Member', 'Two guided walks', 'Crew-day priority'],
    recommended: true,
  },
  {
    value: 'member',
    name: 'Member',
    amount: '45',
    summary: 'Free parking and the quarterly newsletter.',
    features: ['Free parking', 'Quarterly newsletter'],
  },
  {
    value: 'patron',
    name: 'Patron',
    amount: '500',
    summary: 'Sold out for this season.',
    features: ['Everything in Steward', 'Annual preserve tour'],
    unavailable: true,
  },
]

/** A membership tier set chosen with a Radio group, then a wholesale matrix. */
export function PricingTiers() {
  return (
    <div className={styles.stack}>
      <Pricing kind="tiers">
        <RadioGroup aria-label="Membership level" defaultValue="steward">
          <PricingTierList>
            {tiers.map((tier) => (
              <PricingTier key={tier.value} recommended={tier.recommended}>
                <PricingTierName>{tier.name}</PricingTierName>
                <PricingPrice value={tier.amount}>
                  <PricingCurrency>$</PricingCurrency>
                  <PricingAmount>{tier.amount}</PricingAmount>
                  <PricingPeriod>/ year</PricingPeriod>
                </PricingPrice>
                <PricingSummary>{tier.summary}</PricingSummary>
                <PricingFeatures>
                  {tier.features.map((feature) => (
                    <PricingFeature key={feature}>{feature}</PricingFeature>
                  ))}
                </PricingFeatures>
                <PricingChoice>
                  <Radio value={tier.value} disabled={tier.unavailable}>
                    Choose {tier.name}
                  </Radio>
                </PricingChoice>
                {tier.unavailable ? <PricingAction>Unavailable</PricingAction> : null}
              </PricingTier>
            ))}
          </PricingTierList>
        </RadioGroup>
        <PricingAsOf>Prices as of 1 Sep 2026 (hemlockravine.org/join)</PricingAsOf>
      </Pricing>

      <Pricing kind="matrix">
        <PricingMatrix label="Seedling prices">
          <thead>
            <tr>
              <th scope="col">Species</th>
              <th scope="col">Retail</th>
              <th scope="col">Wholesale</th>
            </tr>
          </thead>
          <tbody>
            <tr>
              <th scope="row">Pawpaw</th>
              <PricingCell label="Retail">$18</PricingCell>
              <PricingCell label="Wholesale">$11</PricingCell>
            </tr>
            <tr>
              <th scope="row">Serviceberry</th>
              <PricingCell label="Retail">$14</PricingCell>
              <PricingCell label="Wholesale">$9</PricingCell>
            </tr>
          </tbody>
          <tfoot>
            <tr>
              <th scope="row">Minimum order</th>
              <PricingCell label="Retail">1</PricingCell>
              <PricingCell label="Wholesale">25</PricingCell>
            </tr>
          </tfoot>
        </PricingMatrix>
      </Pricing>

      <Button variant="solid">Join Now</Button>
    </div>
  )
}

Grounds and print

Light grounds are the home for tiers; inside a field, tier faces become light islands. Each tier's rest edge is its face's own edge color, so it stays visible on every ground. Saturated grounds hold a single price only. In print the recommended frame keeps 2.25 pt and other edges 0.75 pt, the badge prints as a word above its tier, and actions print their short URLs.

API Reference

Pricing

The pricing module. Inline, menu and sale prices hold a PricingPrice (and, for a sale, a PricingStruck and a PricingSave); pass render={<span />} inside running text. A tier set holds a PricingTierList of PricingTiers; a matrix a PricingMatrix. Close a tier set or matrix with PricingAsOf. Show the unit and period with every price, never mark a sale with red alone, and never hide fees.

PropTypeDescription
kind
PricingKind

Required. inline: “From $49″ in running text, figure bold. menu: a mono figure on its own line or at a leader row’s end (§12.13). sale: the struck original in --role-muted, the current price bold, then “Save $300″ as a word. tiers: 2–4 plans as faced cards, stacked, as columns from 1024 px of container. matrix: rows × columns of prices in a table, one block per row below 360 px of container.

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: prices, strikes, frames and checks. 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 tier Radio’s selection (--role-select) and the badge. Never defaulted.

PricingPrice and its parts

One price, a data element: PricingQualifier (“From”), PricingCurrency, PricingAmount, PricingCents and PricingPeriod, in the locale’s order (author the spaces). Kept on one line.

PropTypeDescription
label
React.ReactNode | undefined

Words for assistive technology only, read before the price: “Now” for the current price of a sale.

value
string | number | undefined

The machine-readable amount, e.g. "49.00"; written as the data element’s value.

The qualifier, e.g. “From”: regular weight, before the figure.

The currency sign: with the figure inline; type-caption beside a tier’s figure.

The figure, in data numerals: bold in body text (inline, sale), mono (menu), type-stat-compact in a tier.

Optional retail cents as a superscript at 60%, never below --font-size-0.

The period or unit, e.g. “/ month”, after a thin space the part adds; on the figure’s line, type-caption in a tier.

PricingStruck and PricingSave

The original price of a sale, struck through at --ds-stroke-1-5 in --role-muted (never a status color), with a hidden “Was” for assistive technology. Put a PricingPrice inside.

PropTypeDescription
label
React.ReactNode | undefined

Words for assistive technology only, read before the struck price. Default “Was”.

The saving as a word, “Save $300″, in --primary12: the word carries the sale, not a color.

PricingTierList and PricingTier

The tier set’s layout, queried on the pricing container: stacked below 1024 px of container (author the recommended tier first, since visual order follows source order), columns of one comparison row from 1024, capped at --ds-container-content. When the plans feed a form, wrap it in a RadioGroup and put each tier’s Radio in its PricingChoice.

One plan: a faced card (article) holding PricingTierName, a PricingPrice, PricingSummary, PricingFeatures, an optional PricingChoice (a Radio, when the plan feeds a form) and a PricingAction (a Button, “Choose Basic”). Selected and unavailable states follow the Radio’s data-checked and data-disabled.

PropTypeDescription
badge
React.ReactNode | undefined

The badge word straddling the top edge. Defaults to “Recommended” on the recommended tier; pass your own words, or null for none.

frameProps
| Omit<
    useRender.ComponentProps<'article', {}, HTMLProps>,
    'children'
  >
| undefined

Props for the tier’s frame, the faced Card (an article), such as aria-labelledby.

recommended
boolean | undefined

Marks the recommended plan: a --ds-stroke-3 --primary12 frame and the badge. Default false. Never fill-only.

Tier parts

The plan’s name: type-itemhead in --primary12. Renders <h3>.

The one-line summary: type-body-ui in --primary12.

The feature list, in full under each tier. Omit excluded features; never strike them.

One included feature, led by the inline-tier check icon (hidden: the words carry it).

The plan choice, the tier Card’s CardChoice: put the tier’s Radio here (its group wraps the tier list). Its data-checked selects the tier’s edge; its data-disabled marks the tier unavailable (then say “Unavailable” in PricingAction).

The tier’s action: a Button with a title-case label (“Start Free Trial”), or the word “Unavailable”. Prints as “Label (short URL)".

PricingMatrix and PricingCell

The price matrix: a §8.2 table (rows by species or size × retail and wholesale columns) with --role-rule row rules and a line-double-hair total rule above the tfoot (the minimum order). Author thead, tbody and tfoot; use PricingCell with a label so each value keeps its column name when rows become blocks below 360 px of container.

PropTypeDescription
label
string | undefined

Names the scrolling wrapper for assistive technology, which then is a focusable region (so keyboards can scroll a wide matrix).

A matrix value cell; its label repeats the column header in the block form.

PropTypeDescription
label
React.ReactNode | undefined

The column’s name (“Wholesale”), shown before the value only when rows are blocks.

PricingAsOf

The provenance line, “Prices as of 1 Sep 2026 (short URL)": type-caption after a tier set or matrix, on screen and in print (§12.9, P10).

Additional types

pricing
PricingActionProps

Props for PricingAction: div props and render.

type PricingActionProps = useRender.ComponentProps<'div'>
PricingAsOfProps

Props for PricingAsOf: paragraph props and render.

type PricingAsOfProps = useRender.ComponentProps<'p'>
PricingCellProps

Props for PricingCell: td props, render and the column name.

type PricingCellProps = React.ClassAttributes<HTMLTableDataCellElement> &
  React.TdHTMLAttributes<HTMLTableDataCellElement> &
  __type & { label?: React.ReactNode }
PricingChoiceProps

Props for PricingChoice: div props and render.

type PricingChoiceProps = useRender.ComponentProps<'div'>
PricingFeatureProps

Props for PricingFeature: list-item props and render.

type PricingFeatureProps = useRender.ComponentProps<'li'>
PricingFeaturesProps

Props for PricingFeatures: list props and render.

type PricingFeaturesProps = useRender.ComponentProps<'ul'>
PricingKind

The five pricing builds (§12.9).

type PricingKind = 'inline' | 'menu' | 'sale' | 'tiers' | 'matrix'
PricingMatrixProps

Props for PricingMatrix: table props, render and the scroll region’s name.

type PricingMatrixProps = React.ClassAttributes<HTMLTableElement> &
  React.TableHTMLAttributes<HTMLTableElement> &
  __type & { label?: string }
PricingPartProps

Props for the small price parts: span props and render.

type PricingPartProps = useRender.ComponentProps<'span'>
PricingPriceProps

Props for PricingPrice: data props, render and the hidden label.

type PricingPriceProps = React.ClassAttributes<HTMLDataElement> &
  React.DataHTMLAttributes<HTMLDataElement> &
  __type & { value?: string | number; label?: React.ReactNode }
PricingProps

Props for Pricing: div props, render, the kind and the color axes.

type PricingProps = React.ClassAttributes<HTMLDivElement> &
  React.HTMLAttributes<HTMLDivElement> &
  __type & {
    kind: PricingKind;
    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;
  }
PricingStruckProps

Props for PricingStruck: s props, render and the hidden label.

type PricingStruckProps = React.ClassAttributes<HTMLElement> &
  React.HTMLAttributes<HTMLElement> &
  __type & { label?: React.ReactNode }
PricingSummaryProps

Props for PricingSummary: paragraph props and render.

type PricingSummaryProps = useRender.ComponentProps<'p'>
pricingTier

Tier classes: recommended draws the --ds-stroke-3 --primary12 frame and the badge.

type pricingTier = pricingTier
PricingTierListProps

Props for PricingTierList: list props and render.

type PricingTierListProps = useRender.ComponentProps<'ul'>
PricingTierNameProps

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

type PricingTierNameProps = useRender.ComponentProps<'h3'>
PricingTierProps

Props for PricingTier: list-item props, render, recommended and the badge word.

type PricingTierProps = React.ClassAttributes<HTMLLIElement> &
  React.LiHTMLAttributes<HTMLLIElement> &
  __type &
  VariantProps<__type> & {
    recommended?: boolean;
    badge?: React.ReactNode;
    frameProps?: Omit<useRender.ComponentProps<'article', {}, HTMLProps>, 'children'>;
  }

Specification: DESIGN-SYSTEM.md §12.9 (pricing, v1 per D169) and §5.10.2 (module reflow).