Product mosaic hero
Three offset panels built from live components rather than a screenshot.
Live preview
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.
Related
All sectionsSplit hero with live panel
Copy on the left, a real component composition on the right — not a screenshot.
intermediate3 variantsDashboard metric row
Four KPI tiles, two of them with inverted trend semantics.
starterFeatured3 variantsSaaS landing page
Eleven blocks in the order a subscription product needs: claim, proof, mechanism, evidence, price, objections, action.
intermediateFeatured4 variants