Calm Founder Landing Page with Karaoke Audio Narration
A prompt for building a calm, editorial single-page founder site with synchronised audio narration that highlights words as they're spoken.
- landing page
- react
- tailwind
- audio
PROMPT — Calm founder landing page with karaoke audio narration
Build a single-page React + TypeScript + Vite + Tailwind site. The aesthetic is calm and editorial: generous whitespace, Inter typography, no dividers, no drop shadows, no heavy borders. Everything must use semantic HSL design tokens — never raw color classes.
Stack
- React 18, TypeScript, Vite
- Tailwind CSS v3 + shadcn/ui
- React Router v6
- lucide-react icons
- Inter font from Google Fonts (weights 400/500/600/700)
Design tokens (src/index.css)
Define on :root (light) and .dark:
Light:
--background: 0 0% 100%;
--foreground: 220 30% 18%;
--primary: 220 30% 18%;
--primary-foreground: 0 0% 100%;
--muted: 220 15% 96%;
--muted-foreground: 220 10% 50%;
--border: 220 15% 90%;
--ring: 220 30% 18%;
--radius: 0.25rem;
--text-body: 220 15% 30%;
--text-subtle: 220 10% 55%;
--link-hover: 220 60% 45%;
Dark:
--background: 220 30% 8%;
--foreground: 220 15% 95%;
--muted: 220 20% 15%;
--border: 220 20% 18%;
--text-body: 220 10% 75%;
--text-subtle: 220 10% 55%;
--link-hover: 220 60% 65%;
Body uses Inter. Add helper classes:
- .text-body → text-[hsl(var(--text-body))]
- .text-subtle → text-[hsl(var(--text-subtle))]
- .link-subtle → text-foreground font-bold transition-colors duration-200 hover:text-[hsl(var(--link-hover))]
Expose body and subtle color aliases in tailwind.config.ts so text-body / text-subtle work as Tailwind utilities.
Layout (/ page)
Centered column, max-w-xl mx-auto px-6 py-12 md:py-24:
1. 64px square logo, rounded-xl, top-left.
2. <NarrationPlayer> (see below) wrapping the founder bio (4 short paragraphs in first person, ending with "— Alex"). The first paragraph contains an inline link to the App Store with a small ExternalLink icon (lucide).
3. A row of social links.
4. A subtle location line: lucide Coffee icon + a short phrase, in text-xs text-subtle.
5. A faded full-width wordmark image at opacity-[0.04], pointer-events-none select-none, as a quiet background flourish near the bottom.
6. Footer: © 2026 [Brand] left; Privacy / Terms right; all text-xs text-subtle, hover transitions to foreground.
Also add /privacy, /terms, and a 404 route. Mount a ScrollToTop helper inside the router.
NarrationPlayer component (the key piece)
File: src/components/NarrationPlayer.tsx. Behavior:
- Renders a hidden <audio preload="metadata" src="/audio/founder-narration.m4a" />. Place a ~30–60s narration MP4/M4A in public/audio/.
- Renders a title row: an <h1 class="text-lg text-foreground"> with the speaker name, plus a 28px circular play/pause button (bg-muted/50 hover:bg-muted, rounded-full) using lucide Volume2 (idle/paused) and Pause (playing) icons at w-3.5 h-3.5.
- Renders children inside <article class="mb-10 narration-text [&_p]:mb-6">. Adds class narration-active to the article whenever audio is playing or paused mid-track.
Karaoke logic:
1. On mount, recursively walk the article DOM. For every text node, split on /(\s+)/ and wrap each non-whitespace token in <span class="narration-word" data-word>. Preserve whitespace text nodes and skip nodes that are already wrapped. Inline elements (like the App Store link) must be traversed into, not replaced.
2. Count totalWords = querySelectorAll('[data-word]').length.
3. While playing, run a requestAnimationFrame loop. Each frame:
const PACE_MULTIPLIER = 0.90; // compensates for natural pauses
const progress = audio.currentTime / audio.duration;
const adjusted = Math.min(progress * PACE_MULTIPLIER, 1);
const highlightIndex = Math.floor(adjusted * totalWords);
4. When highlightIndex changes, iterate every word span:
- clear is-active and is-upcoming
- if index < highlightIndex → add is-active
- else if index < highlightIndex + 6 → add is-upcoming
5. Audio events: play → start RAF + set state playing; pause → cancel RAF; ended → cancel RAF, reset highlightIndex = -1. Click handler: idle/paused → play(); playing → pause(); ended → reset currentTime = 0 and play().
6. If window.matchMedia('(prefers-reduced-motion: reduce)').matches, also add reduced-motion to each word span.
Karaoke CSS (append to src/index.css):
.narration-text .narration-word {
opacity: 1;
transition: opacity 250ms linear, color 250ms linear, font-weight 250ms linear;
}
.narration-text.narration-active .narration-word { opacity: 0.35; }
.narration-text.narration-active .narration-word.is-upcoming { opacity: 0.55; }
.narration-text.narration-active .narration-word.is-active {
opacity: 1;
color: hsl(var(--foreground));
font-weight: 500;
}
.narration-word.reduced-motion { transition: none !important; }
.narration-button:focus-visible {
outline: 2px solid hsl(var(--ring));
outline-offset: 2px;
}
Effect: when the user presses play, every word fades to 35% opacity, the words "spoken so far" snap to full foreground color and weight 500, and the next ~6 words sit at 55% opacity as a soft look-ahead. When idle or ended, everything returns to its natural readable state.
SEO / meta (index.html)
- Title <60 chars, meta description <160 chars
- Open Graph + Twitter card tags, canonical link, responsive viewport
- Optional Apple smart app banner meta if linking to an iOS app
- JSON-LD SoftwareApplication block if applicable
Hard rules
- Only semantic tokens — no text-white, bg-black, hex, or RGB anywhere in components.
- All colors stored as raw HSL triplets in index.css, consumed via hsl(var(--token)).
- Never autoplay audio. Playback only on user click.
- No borders/dividers between sections; rely on spacing for rhythm.