Skip to content

Hero with statistics band

A claim above a four-figure band, with every figure read from the live catalogue.

MarketingstarterFeaturedherostatsproofcounts

Live preview

full widthLive preview — open it in a new tab for the full-height version.
Open the preview in a new tab

Source

This exact file renders the preview above.

import { ArrowRight } from 'lucide-react'
import { ButtonLink } from '@/components/ui/button'
import { Container } from '@/components/ui/layout'
import { catalogueCounts } from '@/lib/catalogue'

/**
 * Hero with a statistics band
 *
 * The claim and the evidence in one block. Every figure here is read from the
 * live catalogue rather than typed in, which is why the section can never
 * overstate what the library contains — see `/docs/contributing`.
 */
export default function StatsBandHero() {
  const figures = [
    { value: String(catalogueCounts.component), label: 'Primitives' },
    { value: String(catalogueCounts.section), label: 'Sections' },
    { value: String(catalogueCounts.form), label: 'Form flows' },
    { value: String(catalogueCounts.starter), label: 'Starter products' },
  ]

  return (
    <section className="border-b border-line bg-canvas">
      <Container className="py-section">
        <div className="max-w-3xl">
          <h2 className="display-type text-3xl leading-tight font-semibold text-ink-strong sm:text-4xl lg:text-5xl">
            Enough building blocks to finish the whole product.
          </h2>
          <p className="mt-5 max-w-xl text-md text-ink-muted">
            Not a starter kit with twelve components and a promise. Four levels of composition, from
            a button to a ten-route product.
          </p>
          <ButtonLink
            href="/starters"
            className="mt-8"
            trailingIcon={<ArrowRight className="size-4" />}
          >
            See the starters
          </ButtonLink>
        </div>
      </Container>

      <div className="border-t border-line bg-surface-sunken">
        <Container>
          <dl className="grid grid-cols-2 divide-x divide-y divide-[var(--color-border)] sm:grid-cols-4 sm:divide-y-0">
            {figures.map((figure) => (
              <div key={figure.label} className="px-2 py-8 text-center first:border-l-0 sm:px-4">
                <dd className="display-type text-3xl font-semibold text-ink-strong tabular-nums sm:text-4xl">
                  {figure.value}
                </dd>
                <dt className="label-caps mt-2 text-ink-subtle">{figure.label}</dt>
              </div>
            ))}
          </dl>
        </Container>
      </div>
    </section>
  )
}

components/blocks/sections/hero/stats-band.tsx

import { ArrowRight } from 'lucide-react'
import { ButtonLink } from '@/components/ui/button'
import { Container } from '@/components/ui/layout'
import { catalogueCounts } from '@/lib/catalogue'

/**
 * Hero with a statistics band
 *
 * The claim and the evidence in one block. Every figure here is read from the
 * live catalogue rather than typed in, which is why the section can never
 * overstate what the library contains — see `/docs/contributing`.
 */
export default function StatsBandHero() {
  const figures = [
    { value: String(catalogueCounts.component), label: 'Primitives' },
    { value: String(catalogueCounts.section), label: 'Sections' },
    { value: String(catalogueCounts.form), label: 'Form flows' },
    { value: String(catalogueCounts.starter), label: 'Starter products' },
  ]

  return (
    <section className="border-b border-line bg-canvas">
      <Container className="py-section">
        <div className="max-w-3xl">
          <h2 className="display-type text-3xl leading-tight font-semibold text-ink-strong sm:text-4xl lg:text-5xl">
            Enough building blocks to finish the whole product.
          </h2>
          <p className="mt-5 max-w-xl text-md text-ink-muted">
            Not a starter kit with twelve components and a promise. Four levels of composition, from
            a button to a ten-route product.
          </p>
          <ButtonLink
            href="/starters"
            className="mt-8"
            trailingIcon={<ArrowRight className="size-4" />}
          >
            See the starters
          </ButtonLink>
        </div>
      </Container>

      <div className="border-t border-line bg-surface-sunken">
        <Container>
          <dl className="grid grid-cols-2 divide-x divide-y divide-[var(--color-border)] sm:grid-cols-4 sm:divide-y-0">
            {figures.map((figure) => (
              <div key={figure.label} className="px-2 py-8 text-center first:border-l-0 sm:px-4">
                <dd className="display-type text-3xl font-semibold text-ink-strong tabular-nums sm:text-4xl">
                  {figure.value}
                </dd>
                <dt className="label-caps mt-2 text-ink-subtle">{figure.label}</dt>
              </div>
            ))}
          </dl>
        </Container>
      </div>
    </section>
  )
}

lib/catalogue/index.ts

import { componentItems } from './components'
import { navigationItems } from './navigation'
import { formItems } from './forms'
import { sectionItems } from './sections'
import { patternItems } from './patterns'
import { starterItems } from './starters'
import { families, familyBySegment, familyByName } from './families'
import type { CatalogueFamily, CatalogueItem, FamilyDefinition } from './types'

export * from './types'
export { families, familyBySegment, familyByName } from './families'

/**
 * The catalogue graph.
 *
 * One flat array plus a handful of indexes. Every browse, filter, search and
 * related-content surface in the application reads from these — which is why
 * adding an item never requires touching a page component.
 */
export const catalogue: CatalogueItem[] = [
  ...componentItems,
  ...navigationItems,
  ...formItems,
  ...sectionItems,
  ...patternItems,
  ...starterItems,
]

export const catalogueById = new Map(catalogue.map((item) => [item.id, item]))

export const catalogueByFamily = new Map<CatalogueFamily, CatalogueItem[]>(
  families.map((family) => [
    family.family,
    catalogue.filter((item) => item.family === family.family),
  ]),
)

export function itemsForFamily(family: CatalogueFamily): CatalogueItem[] {
  return catalogueByFamily.get(family) ?? []
}

export function itemsForSegment(segment: string): CatalogueItem[] {
  const family = familyBySegment.get(segment)
  return family ? itemsForFamily(family.family) : []
}

export function findItem(family: CatalogueFamily, slug: string): CatalogueItem | undefined {
  return catalogueById.get(`${family}/${slug}`)
}

export function resolveRelated(item: CatalogueItem): CatalogueItem[] {
  return item.relatedItems
    .map((id) => catalogueById.get(id))
    .filter((candidate): candidate is CatalogueItem => Boolean(candidate))
}

/** Route segment for an item, e.g. `/components/button`. */
export function hrefForItem(item: CatalogueItem): string {
  const family = familyByName.get(item.family)
  return `/${family?.segment ?? 'components'}/${item.slug}`
}

export function categoriesForFamily(family: CatalogueFamily): FamilyDefinition['categories'] {
  return familyByName.get(family)?.categories ?? []
}

/** Live counts. Never hard-code a marketing number — derive it from here. */
export const catalogueCounts = {
  component: itemsForFamily('component').length,
  navigation: itemsForFamily('navigation').length,
  form: itemsForFamily('form').length,
  section: itemsForFamily('section').length,
  pattern: itemsForFamily('pattern').length,
  starter: itemsForFamily('starter').length,
  total: catalogue.length,
} as const

export function countForCategory(family: CatalogueFamily, category: string): number {
  return itemsForFamily(family).filter((item) => item.category === category).length
}

export const featuredItems = catalogue.filter((item) => item.featured)

/** Every tag in use, with its frequency, most used first. */
export const tagIndex: Array<{ tag: string; count: number }> = (() => {
  const counts = new Map<string, number>()
  for (const item of catalogue) {
    for (const tag of item.tags) counts.set(tag, (counts.get(tag) ?? 0) + 1)
  }
  return Array.from(counts.entries())
    .map(([tag, count]) => ({ tag, count }))
    .sort((a, b) => b.count - a.count || a.tag.localeCompare(b.tag))
})()

Demo source — adapt to your project. Foundry is not published as a package.

Usage

Claim and evidence in one block. The figures come from the catalogue itself, so the section can never overstate what the library contains.

  • Never hard-code a marketing number — derive it, or the first person to count will find the gap.
  • Four figures maximum; a fifth stops being scannable.

Variants and states

Every entry below is a genuine difference in behaviour or layout, and every one of them is visible in the preview above.

  • Claim block
  • Divided figure band
  • Counts derived at build time

Accessibility

Definition list
The band is a dl, pairing each figure with its label programmatically.
Tabular figures
Numerals use tabular-nums so the band does not jitter between palettes.

Foundry implements published ARIA patterns and is tested against them. No WCAG certification is claimed — see the accessibility documentation for what is and is not covered.