Skip to content

Product mosaic hero

Three offset panels built from live components rather than a screenshot.

MarketingintermediateFeaturedheroproductdashboardmosaic

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 { Badge } from '@/components/ui/badge'
import { Metric } from '@/components/ui/metric'
import { Status } from '@/components/ui/status'
import { Progress } from '@/components/ui/progress'

/**
 * Product mosaic hero
 *
 * Three offset panels built from real components rather than an image. Because
 * the mosaic is live UI, it re-themes with the palette and can never become a
 * screenshot of an older release — the failure mode of every image-led hero.
 */
export default function ProductMosaicHero() {
  return (
    <section className="overflow-hidden border-b border-line bg-canvas py-section">
      <Container>
        <div className="mx-auto max-w-2xl text-center">
          <h2 className="display-type text-3xl leading-tight font-semibold text-ink-strong sm:text-4xl">
            Dashboards that were not designed twice.
          </h2>
          <p className="mx-auto mt-4 max-w-lg text-md text-ink-muted">
            The same primitives behind your marketing site, at compact density.
          </p>
          <ButtonLink
            href="/sections?category=application"
            className="mt-7"
            trailingIcon={<ArrowRight className="size-4" />}
          >
            Application sections
          </ButtonLink>
        </div>

        <div className="mt-14 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
          <div className="rounded-xl border border-line bg-surface p-5 shadow-sm lg:mt-8">
            <Metric
              label="Monthly revenue"
              value="$184,200"
              delta={12.4}
              sparkline={[12, 18, 15, 22, 26, 24, 31, 34]}
              appearance="plain"
            />
          </div>

          <div className="rounded-xl border border-line bg-surface p-5 shadow-sm">
            <div className="flex items-center justify-between">
              <p className="text-sm font-semibold text-ink-strong">Services</p>
              <Badge tone="success" dot>
                Healthy
              </Badge>
            </div>
            <ul className="mt-4 flex flex-col gap-2.5">
              <li>
                <Status appearance="dot" kind="operational" label="api.foundry.dev" />
              </li>
              <li>
                <Status appearance="dot" kind="operational" label="cdn.foundry.dev" />
              </li>
              <li>
                <Status appearance="dot" kind="degraded" label="webhooks.foundry.dev" />
              </li>
            </ul>
          </div>

          <div className="rounded-xl border border-line bg-surface p-5 shadow-sm lg:mt-16">
            <p className="text-sm font-semibold text-ink-strong">Migration</p>
            <div className="mt-4 flex flex-col gap-3">
              <Progress value={100} tone="success" label="Tokens" showValue />
              <Progress value={68} label="Components" showValue />
              <Progress value={22} tone="warning" label="Sections" showValue />
            </div>
          </div>
        </div>
      </Container>
    </section>
  )
}

components/blocks/sections/hero/product-mosaic.tsx

import { ArrowRight } from 'lucide-react'
import { ButtonLink } from '@/components/ui/button'
import { Container } from '@/components/ui/layout'
import { Badge } from '@/components/ui/badge'
import { Metric } from '@/components/ui/metric'
import { Status } from '@/components/ui/status'
import { Progress } from '@/components/ui/progress'

/**
 * Product mosaic hero
 *
 * Three offset panels built from real components rather than an image. Because
 * the mosaic is live UI, it re-themes with the palette and can never become a
 * screenshot of an older release — the failure mode of every image-led hero.
 */
export default function ProductMosaicHero() {
  return (
    <section className="overflow-hidden border-b border-line bg-canvas py-section">
      <Container>
        <div className="mx-auto max-w-2xl text-center">
          <h2 className="display-type text-3xl leading-tight font-semibold text-ink-strong sm:text-4xl">
            Dashboards that were not designed twice.
          </h2>
          <p className="mx-auto mt-4 max-w-lg text-md text-ink-muted">
            The same primitives behind your marketing site, at compact density.
          </p>
          <ButtonLink
            href="/sections?category=application"
            className="mt-7"
            trailingIcon={<ArrowRight className="size-4" />}
          >
            Application sections
          </ButtonLink>
        </div>

        <div className="mt-14 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
          <div className="rounded-xl border border-line bg-surface p-5 shadow-sm lg:mt-8">
            <Metric
              label="Monthly revenue"
              value="$184,200"
              delta={12.4}
              sparkline={[12, 18, 15, 22, 26, 24, 31, 34]}
              appearance="plain"
            />
          </div>

          <div className="rounded-xl border border-line bg-surface p-5 shadow-sm">
            <div className="flex items-center justify-between">
              <p className="text-sm font-semibold text-ink-strong">Services</p>
              <Badge tone="success" dot>
                Healthy
              </Badge>
            </div>
            <ul className="mt-4 flex flex-col gap-2.5">
              <li>
                <Status appearance="dot" kind="operational" label="api.foundry.dev" />
              </li>
              <li>
                <Status appearance="dot" kind="operational" label="cdn.foundry.dev" />
              </li>
              <li>
                <Status appearance="dot" kind="degraded" label="webhooks.foundry.dev" />
              </li>
            </ul>
          </div>

          <div className="rounded-xl border border-line bg-surface p-5 shadow-sm lg:mt-16">
            <p className="text-sm font-semibold text-ink-strong">Migration</p>
            <div className="mt-4 flex flex-col gap-3">
              <Progress value={100} tone="success" label="Tokens" showValue />
              <Progress value={68} label="Components" showValue />
              <Progress value={22} tone="warning" label="Sections" showValue />
            </div>
          </div>
        </div>
      </Container>
    </section>
  )
}

components/ui/metric.tsx

import type { ReactNode } from 'react'
import { ArrowUpRight, ArrowDownRight, Minus } from 'lucide-react'
import { cn } from '@/lib/cn'

/**
 * Metric
 *
 * A single number with optional trend. The delta carries an arrow *and* a sign
 * *and* a colour, so the direction survives a greyscale print or a colour
 * vision deficiency.
 *
 * `<dl>` semantics pair the label with the value; the sparkline is decorative
 * and hidden from assistive tech, since the number beside it is the content.
 */
export interface MetricProps {
  label: string
  value: string
  /** Percentage change. Positive is not automatically "good" — see `invertTrend`. */
  delta?: number
  deltaLabel?: string
  /** For metrics where down is good, e.g. churn or latency. */
  invertTrend?: boolean
  icon?: ReactNode
  /** 0–1 values rendered as a decorative sparkline. */
  sparkline?: number[]
  appearance?: 'plain' | 'card'
  className?: string
}

function Sparkline({ points }: { points: number[] }) {
  if (points.length < 2) return null
  const max = Math.max(...points)
  const min = Math.min(...points)
  const range = max - min || 1
  const path = points
    .map((point, index) => {
      const x = (index / (points.length - 1)) * 100
      const y = 28 - ((point - min) / range) * 26 - 1
      return `${index === 0 ? 'M' : 'L'}${x.toFixed(2)},${y.toFixed(2)}`
    })
    .join(' ')

  return (
    <svg viewBox="0 0 100 28" preserveAspectRatio="none" className="h-7 w-full" aria-hidden="true">
      <path
        d={path}
        fill="none"
        stroke="var(--color-accent)"
        strokeWidth="1.5"
        vectorEffect="non-scaling-stroke"
      />
    </svg>
  )
}

export function Metric({
  label,
  value,
  delta,
  deltaLabel,
  invertTrend = false,
  icon,
  sparkline,
  appearance = 'card',
  className,
}: MetricProps) {
  const direction = delta === undefined ? 'flat' : delta > 0 ? 'up' : delta < 0 ? 'down' : 'flat'
  const good = direction === 'flat' ? null : invertTrend ? direction === 'down' : direction === 'up'
  const TrendIcon =
    direction === 'up' ? ArrowUpRight : direction === 'down' ? ArrowDownRight : Minus

  return (
    <dl
      className={cn(
        'flex min-w-0 flex-col gap-1',
        appearance === 'card' && 'rounded-lg border border-line bg-surface p-card',
        className,
      )}
    >
      <div className="flex items-center justify-between gap-2">
        <dt className="label-caps truncate text-ink-subtle">{label}</dt>
        {icon ? <span className="shrink-0 text-ink-subtle">{icon}</span> : null}
      </div>
      <dd className="display-type text-2xl leading-none font-semibold text-ink-strong tabular-nums">
        {value}
      </dd>
      {delta !== undefined ? (
        <dd
          className={cn(
            'flex items-center gap-1 text-xs font-medium',
            good === null ? 'text-ink-muted' : good ? 'text-success' : 'text-danger',
          )}
        >
          <TrendIcon className="size-3.5 shrink-0" aria-hidden="true" />
          <span>
            {delta > 0 ? '+' : ''}
            {delta}%
          </span>
          {deltaLabel ? <span className="text-ink-subtle">{deltaLabel}</span> : null}
        </dd>
      ) : null}
      {sparkline ? (
        <dd className="mt-1">
          <Sparkline points={sparkline} />
        </dd>
      ) : null}
    </dl>
  )
}

components/ui/status.tsx

import { CheckCircle2, AlertTriangle, XCircle, Info, CircleDashed, Clock } from 'lucide-react'
import { cn } from '@/lib/cn'

/**
 * Status
 *
 * Deliberately separate from Badge. A status describes the *state of a thing*
 * (a deployment, an invoice, a service) and therefore always pairs an icon
 * with a label — colour is the third signal, never the only one. That rule is
 * what makes the library legible to colour-blind users without a theme switch.
 */
export type StatusKind = 'operational' | 'degraded' | 'down' | 'pending' | 'info' | 'idle'

export interface StatusProps {
  kind: StatusKind
  label: string
  /** `dot` for dense tables, `pill` for standalone display. */
  appearance?: 'dot' | 'pill' | 'inline'
  className?: string
}

const config = {
  operational: {
    icon: CheckCircle2,
    text: 'text-success',
    bg: 'bg-success-soft border-success-line',
    dot: 'bg-success',
  },
  degraded: {
    icon: AlertTriangle,
    text: 'text-warning',
    bg: 'bg-warning-soft border-warning-line',
    dot: 'bg-warning',
  },
  down: {
    icon: XCircle,
    text: 'text-danger',
    bg: 'bg-danger-soft border-danger-line',
    dot: 'bg-danger',
  },
  pending: { icon: Clock, text: 'text-info', bg: 'bg-info-soft border-info-line', dot: 'bg-info' },
  info: { icon: Info, text: 'text-info', bg: 'bg-info-soft border-info-line', dot: 'bg-info' },
  idle: {
    icon: CircleDashed,
    text: 'text-ink-subtle',
    bg: 'bg-surface-sunken border-line',
    dot: 'bg-ink-subtle',
  },
} as const

export function Status({ kind, label, appearance = 'inline', className }: StatusProps) {
  const entry = config[kind]
  const Icon = entry.icon

  if (appearance === 'dot') {
    return (
      <span className={cn('inline-flex items-center gap-2 text-sm text-ink', className)}>
        <span className={cn('size-2 shrink-0 rounded-full', entry.dot)} aria-hidden="true" />
        {label}
      </span>
    )
  }

  if (appearance === 'pill') {
    return (
      <span
        className={cn(
          'inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-xs font-medium',
          entry.bg,
          entry.text,
          className,
        )}
      >
        <Icon className="size-3.5 shrink-0" aria-hidden="true" />
        {label}
      </span>
    )
  }

  return (
    <span
      className={cn('inline-flex items-center gap-1.5 text-sm font-medium', entry.text, className)}
    >
      <Icon className="size-4 shrink-0" aria-hidden="true" />
      {label}
    </span>
  )
}

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

Usage

Because the mosaic is real UI, it re-themes with the palette and can never become a screenshot of an older release — the failure mode of every image-led hero.

  • Offsets are applied only at lg; on mobile the panels stack evenly.
  • Keep the panels shallow — three at most, or the hero becomes the dashboard.

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.

  • Offset panel grid
  • Live metric, status and progress
  • Collapses to one column

Accessibility

Real content
Figures inside the panels are readable text, not baked into an image.
Source order
Panels follow the visual order, so nothing is announced out of sequence.

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.