Skip to content

Product launch page

A single moment, with what shipped before what is next and an interactive preview in the middle.

LandingintermediateFeaturedlaunchreleaseproductinteractive

Live preview

Open the full page
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 TransparentHeroNavbar from '@/components/blocks/navigation/transparent-hero'
import ProductLaunchHeader from '@/components/blocks/headers/product-launch'
import MediaFrameHero from '@/components/blocks/sections/hero/media-frame'
import SpotlightFeature from '@/components/blocks/sections/features/spotlight'
import InteractivePreviewFeature from '@/components/blocks/sections/features/interactive-preview'
import ReleaseLogTimeline from '@/components/blocks/sections/timeline/release-log'
import HorizontalMilestonesTimeline from '@/components/blocks/sections/timeline/horizontal-milestones'
import TestimonialCarousel from '@/components/blocks/sections/testimonials/carousel'
import InvertedBandNewsletter from '@/components/blocks/sections/newsletter/inverted-band'
import MinimalFooter from '@/components/blocks/footers/minimal'

/**
 * Product launch page
 *
 * Built around a single moment. What shipped comes before what is next, and
 * the interactive preview sits in the middle where a reader who is already
 * convinced can stop reading and start playing.
 */
export default function ProductLaunchPattern() {
  return (
    <>
      <TransparentHeroNavbar />
      <main>
        <ProductLaunchHeader />
        <MediaFrameHero />
        <SpotlightFeature />
        <InteractivePreviewFeature />
        <ReleaseLogTimeline />
        <HorizontalMilestonesTimeline />
        <TestimonialCarousel />
        <InvertedBandNewsletter />
      </main>
      <MinimalFooter />
    </>
  )
}

components/patterns/product-launch.tsx

import TransparentHeroNavbar from '@/components/blocks/navigation/transparent-hero'
import ProductLaunchHeader from '@/components/blocks/headers/product-launch'
import MediaFrameHero from '@/components/blocks/sections/hero/media-frame'
import SpotlightFeature from '@/components/blocks/sections/features/spotlight'
import InteractivePreviewFeature from '@/components/blocks/sections/features/interactive-preview'
import ReleaseLogTimeline from '@/components/blocks/sections/timeline/release-log'
import HorizontalMilestonesTimeline from '@/components/blocks/sections/timeline/horizontal-milestones'
import TestimonialCarousel from '@/components/blocks/sections/testimonials/carousel'
import InvertedBandNewsletter from '@/components/blocks/sections/newsletter/inverted-band'
import MinimalFooter from '@/components/blocks/footers/minimal'

/**
 * Product launch page
 *
 * Built around a single moment. What shipped comes before what is next, and
 * the interactive preview sits in the middle where a reader who is already
 * convinced can stop reading and start playing.
 */
export default function ProductLaunchPattern() {
  return (
    <>
      <TransparentHeroNavbar />
      <main>
        <ProductLaunchHeader />
        <MediaFrameHero />
        <SpotlightFeature />
        <InteractivePreviewFeature />
        <ReleaseLogTimeline />
        <HorizontalMilestonesTimeline />
        <TestimonialCarousel />
        <InvertedBandNewsletter />
      </main>
      <MinimalFooter />
    </>
  )
}

components/blocks/headers/product-launch.tsx

import { ArrowRight, Sparkles } from 'lucide-react'
import { Badge } from '@/components/ui/badge'
import { ButtonLink } from '@/components/ui/button'
import { Container } from '@/components/ui/layout'

/**
 * Product launch header
 *
 * Built around a single moment: version, name, one-line promise, one action.
 * The version chip is a real piece of information rather than decoration —
 * launch pages that hide the version are usually hiding the fact that nothing
 * changed.
 */
const highlights = [
  { label: 'Composition levels', value: '4' },
  { label: 'Theme palettes', value: '5' },
  { label: 'Density modes', value: '3' },
]

export default function ProductLaunchHeader() {
  return (
    <header className="relative overflow-hidden border-b border-line bg-canvas py-section">
      <div
        className="pointer-events-none absolute inset-x-0 top-0 h-64 bg-gradient-to-b from-accent-soft to-transparent"
        aria-hidden="true"
      />
      <Container className="relative text-center">
        <Badge
          tone="accent"
          appearance="solid"
          icon={<Sparkles className="size-3" />}
          className="mx-auto"
        >
          Version 1.0
        </Badge>

        <h1 className="display-type mx-auto mt-6 max-w-3xl text-4xl leading-tight font-semibold text-ink-strong sm:text-5xl lg:text-6xl">
          Foundry
        </h1>
        <p className="mx-auto mt-4 max-w-xl text-lg text-ink-muted">
          The complete frontend building-block library. Build faster. Ship better.
        </p>

        <div className="mt-8">
          <ButtonLink href="/components" size="lg" trailingIcon={<ArrowRight className="size-4" />}>
            See what shipped
          </ButtonLink>
        </div>

        <dl className="mx-auto mt-14 grid max-w-2xl grid-cols-3 divide-x divide-[var(--color-border)] border-y border-line">
          {highlights.map((item) => (
            <div key={item.label} className="px-4 py-6">
              <dd className="display-type text-3xl font-semibold text-ink-strong tabular-nums">
                {item.value}
              </dd>
              <dt className="label-caps mt-1.5 text-ink-subtle">{item.label}</dt>
            </div>
          ))}
        </dl>
      </Container>
    </header>
  )
}

components/blocks/sections/features/interactive-preview.tsx

'use client'

import { useState } from 'react'
import { Container } from '@/components/ui/layout'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Card, CardDescription, CardTitle } from '@/components/ui/card'
import { cn } from '@/lib/cn'
import { densities, palettes, type Density, type Palette } from '@/lib/theme'

/**
 * Interactive theme preview
 *
 * A feature section that demonstrates the feature. The preview frame carries
 * its own `data-palette` and `data-density`, so the sample re-themes locally
 * without changing the surrounding page — the same mechanism the catalogue
 * preview frames use.
 */
export default function InteractivePreviewFeature() {
  const [palette, setPalette] = useState<Palette>('editorial')
  const [density, setDensity] = useState<Density>('default')

  return (
    <section className="border-b border-line bg-canvas py-section">
      <Container>
        <div className="max-w-2xl">
          <h2 className="display-type text-2xl font-semibold text-ink-strong sm:text-3xl">
            Change the theme. Watch the components follow.
          </h2>
          <p className="mt-3 text-md text-ink-muted">
            Nothing below is re-implemented per palette. Every difference comes from tokens.
          </p>
        </div>

        <div className="mt-10 grid gap-6 lg:grid-cols-[16rem_1fr]">
          <div className="flex flex-col gap-5">
            <div>
              <h3 className="label-caps mb-2 text-ink-subtle">Palette</h3>
              <div
                role="radiogroup"
                aria-label="Preview palette"
                className="flex flex-wrap gap-1.5"
              >
                {palettes.map((option) => (
                  <button
                    key={option.id}
                    type="button"
                    role="radio"
                    aria-checked={palette === option.id}
                    onClick={() => setPalette(option.id)}
                    className={cn(
                      'rounded-full border px-3 py-1.5 text-xs font-medium transition-colors',
                      palette === option.id
                        ? 'border-accent bg-accent-soft text-accent-soft-ink'
                        : 'border-line text-ink-muted hover:border-line-strong hover:text-ink',
                    )}
                  >
                    {option.label}
                  </button>
                ))}
              </div>
            </div>

            <div>
              <h3 className="label-caps mb-2 text-ink-subtle">Density</h3>
              <div
                role="radiogroup"
                aria-label="Preview density"
                className="flex flex-wrap gap-1.5"
              >
                {densities.map((option) => (
                  <button
                    key={option.id}
                    type="button"
                    role="radio"
                    aria-checked={density === option.id}
                    onClick={() => setDensity(option.id)}
                    className={cn(
                      'rounded-full border px-3 py-1.5 text-xs font-medium transition-colors',
                      density === option.id
                        ? 'border-accent bg-accent-soft text-accent-soft-ink'
                        : 'border-line text-ink-muted hover:border-line-strong hover:text-ink',
                    )}
                  >
                    {option.label}
                  </button>
                ))}
              </div>
            </div>
          </div>

          <div
            data-palette={palette}
            data-density={density}
            className="rounded-xl border border-line bg-canvas p-6 sm:p-8"
          >
            <div className="flex flex-wrap items-center gap-2">
              <Badge tone="accent">{palettes.find((p) => p.id === palette)?.label}</Badge>
              <Badge>{densities.find((d) => d.id === density)?.label}</Badge>
            </div>
            <div className="mt-5 grid gap-4 sm:grid-cols-2">
              <Card tone="raised">
                <CardTitle>Sample card</CardTitle>
                <CardDescription className="mt-1">
                  Radius, padding and shadow are all token-driven.
                </CardDescription>
                <div className="mt-4 flex flex-wrap gap-2">
                  <Button size="sm">Primary</Button>
                  <Button size="sm" variant="outline">
                    Outline
                  </Button>
                </div>
              </Card>
              <Card tone="accent">
                <CardTitle>Accent card</CardTitle>
                <CardDescription className="mt-1">
                  The soft accent trio re-tints with the palette.
                </CardDescription>
                <div className="mt-4 flex flex-wrap gap-2">
                  <Button size="sm" variant="soft">
                    Soft
                  </Button>
                  <Button size="sm" variant="ghost">
                    Ghost
                  </Button>
                </div>
              </Card>
            </div>
          </div>
        </div>
      </Container>
    </section>
  )
}

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

Usage

The interactive preview sits in the middle, where a reader who is already convinced can stop reading and start playing.

  • Put the release timeline before the roadmap — what shipped earns the right to talk about what is next.
  • One action in the hero. A launch page with three calls to action has no launch.

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.

  • Transparent navbar over an inverted hero
  • Reserved media frame
  • Live theme preview
  • Release and roadmap timelines

Accessibility

Reserved media
The media frame declares its aspect ratio, so nothing shifts on load.
Manual carousel
The testimonial carousel never auto-advances, and announces its position.

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.