Hero 9
Full-viewport product hero on a rounded card that zooms out as the page scrolls: an inline nav with brand and actions, a serif headline, sub-copy, dual CTAs, a step-type strip, a drifting WebGL gradient mixed from the theme tokens, a bouncing scroll cue and a carved stat corner. Reduced-motion aware.
Preview
Installation
npx shadcn@latest add https://hirael.com/r/hero-09.jsonCode
components/blocks/hero-09.tsx
'use client';
import * as React from 'react';
import dynamic from 'next/dynamic';
import { Bell, CalendarClock, ChevronDown, Container, FlaskConical, Hammer, Rocket } from 'lucide-react';
import { motion, useReducedMotion, useScroll, useSpring, useTransform } from 'motion/react';
import { Button } from '@/registry/hirael/bases/radix/ui/button';
// WebGL can't paint before hydration, so the shader stays out of the initial
// bundle and the CSS wash below carries the first frame until it arrives.
const Hero09Backdrop = dynamic(() => import('./hero-09-backdrop'), {
ssr: false,
loading: () => null,
});
const NAV_LINKS = ['Features', 'Pipeline', 'Pricing', 'FAQ'];
const STEP_TYPES = [
{ label: 'Build', icon: Hammer },
{ label: 'Test', icon: FlaskConical },
{ label: 'Deploy', icon: Rocket },
{ label: 'Containers', icon: Container },
{ label: 'Notify', icon: Bell },
{ label: 'Schedule', icon: CalendarClock },
];
const STATS = [
{ value: '8', label: 'step types' },
{ value: '40s', label: 'median queue to start' },
{ value: '$0', label: 'to start building' },
];
const BrandMark = ({ className }: { className?: string }) => {
return (
<svg
viewBox="0 0 80 100"
fill="none"
stroke="currentColor"
strokeWidth="2.2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden
className={className}
>
<path d="M16 78 V40 a24 24 0 0 1 48 0 V78" />
<path d="M40 44 L43.2 52 L51 55 L43.2 58 L40 66 L36.8 58 L29 55 L36.8 52 Z" />
<path d="M22 86 H58" opacity="0.7" />
<path d="M28 92 H52" opacity="0.45" />
<path d="M34 96 H46" opacity="0.25" />
</svg>
);
};
const ZOOM_AT = 0.55;
const SPRING = { stiffness: 200, damping: 40, mass: 0.4 };
/**
* Zooms the hero card out as the page scrolls so it recedes behind whatever
* section follows it. Inert when the hero is the only content, and disabled
* under reduced motion.
*/
const HeroZoom = ({ className, children }: React.ComponentProps<'div'>) => {
const reduced = useReducedMotion();
const [viewportHeight, setViewportHeight] = React.useState(0);
const { scrollY } = useScroll();
React.useEffect(() => {
const measure = () => setViewportHeight(window.innerHeight);
measure();
window.addEventListener('resize', measure);
return () => window.removeEventListener('resize', measure);
}, []);
// Until measured, keep the range effectively infinite so progress stays at
// 0 (avoids a first-paint jump before the effect runs).
const end = viewportHeight > 0 ? viewportHeight * ZOOM_AT : 1e9;
const progress = useTransform(scrollY, [0, end], [0, 1], { clamp: true });
const scale = useSpring(useTransform(progress, [0, 1], [1, 0.89]), SPRING);
const opacity = useSpring(useTransform(progress, [0, 1], [1, 0.88]), SPRING);
const borderRadius = useSpring(useTransform(progress, [0, 1], [18, 30]), SPRING);
if (reduced) return <div className={className}>{children}</div>;
return (
<motion.div className={className} style={{ scale, opacity, borderRadius }}>
{children}
</motion.div>
);
};
const Hero09 = () => {
return (
<section data-slot="hero" className="relative min-h-svh bg-background pt-2.5">
<HeroZoom className="sticky top-2.5 mx-2.5 flex min-h-200 origin-top flex-col overflow-hidden rounded-[18px] bg-card will-change-transform lg:h-[calc(100svh-20px)]">
<div aria-hidden className="absolute inset-0">
{/* First frame, and the fallback wherever WebGL is unavailable: amber
overhead, cool blue from the lower start corner, a violet
counterweight opposite it, a warm floor. */}
<div
className="absolute inset-0"
style={{
backgroundImage: [
'radial-gradient(95% 65% at 50% -12%, color-mix(in oklch, var(--primary) 34%, transparent), transparent 62%)',
'radial-gradient(70% 60% at 8% 104%, color-mix(in oklch, var(--accent-cool) 30%, transparent), transparent 66%)',
'radial-gradient(60% 55% at 96% 26%, color-mix(in oklch, var(--chart-2) 20%, transparent), transparent 68%)',
'radial-gradient(80% 50% at 50% 108%, color-mix(in oklch, var(--warm) 18%, transparent), transparent 70%)',
].join(', '),
}}
/>
{/* Fluid-noise gradient, its stops mixed from --background, --primary
and --accent-cool, so it drifts on-palette in either theme. */}
<Hero09Backdrop className="absolute inset-0 size-full" />
{/* Grain keeps the wide gradients from banding on large displays. */}
<div
className="absolute inset-0 opacity-[0.035] mix-blend-overlay"
style={{
backgroundImage:
"url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)'/%3E%3C/svg%3E\")",
}}
/>
</div>
<div className="relative z-10 flex flex-1 flex-col justify-between gap-10 p-6 pb-28 text-foreground md:p-10 md:pt-7">
<div data-slot="hero-nav" className="relative flex items-center justify-between">
<a href="#" data-slot="hero-brand" className="flex items-center gap-2">
<span className="flex size-7 items-center justify-center rounded-sm border border-foreground/20 bg-foreground/10 text-foreground">
<BrandMark className="size-5" />
</span>
<span className="text-base font-semibold tracking-tight">Hirael Flow</span>
</a>
<nav className="absolute start-1/2 hidden items-center gap-6 ltr:-translate-x-1/2 rtl:translate-x-1/2 md:flex">
{NAV_LINKS.map((link) => (
<a
key={link}
href={`#${link.toLowerCase()}`}
className="text-sm text-muted-foreground transition-colors hover:text-foreground"
>
{link}
</a>
))}
</nav>
<div className="flex items-center gap-2">
<Button variant="ghost" size="sm" asChild>
<a href="#">Sign in</a>
</Button>
<Button size="sm" variant="outline" asChild>
<a href="#">Get started</a>
</Button>
</div>
</div>
<div className="mx-auto flex max-w-4xl flex-1 flex-col items-center justify-center gap-6 px-2 text-center">
<h1
data-slot="hero-title"
className="font-serif text-balance text-5xl font-medium leading-[1.05] tracking-tight md:text-6xl lg:text-7xl"
>
Pipelines you can see, <span className="text-primary">not just read</span>
</h1>
<p
data-slot="hero-description"
className="max-w-2xl text-pretty text-base leading-relaxed text-muted-foreground md:text-lg"
>
Design workflows on a canvas, keep them in sync with YAML, and watch every run move step by step through
the same graph.
</p>
<div data-slot="hero-actions" className="flex flex-col gap-3 pt-1 sm:flex-row">
<Button size="lg" asChild>
<a href="#">Start building</a>
</Button>
<Button size="lg" variant="outline" asChild>
<a href="#features">See the features</a>
</Button>
</div>
<div data-slot="hero-steps" className="mt-4 flex flex-col items-center gap-3.5">
<span className="font-mono text-[10px] uppercase tracking-[0.18em] text-muted-foreground">
Every step in one graph
</span>
<div className="flex flex-wrap items-center justify-center gap-x-6 gap-y-3">
{STEP_TYPES.map((step) => (
<span
key={step.label}
className="flex items-center gap-1.5 text-sm font-medium text-muted-foreground transition-colors hover:text-foreground"
>
<step.icon aria-hidden className="size-4 text-primary/70" />
<span className="hidden sm:inline">{step.label}</span>
</span>
))}
</div>
</div>
</div>
</div>
<div className="pointer-events-none absolute inset-x-0 bottom-8 z-10 flex justify-center md:justify-start md:ps-10">
<ChevronDown
aria-hidden
className="size-5 animate-bounce text-muted-foreground/60 motion-reduce:animate-none"
/>
</div>
{/* Carved stat corner: background-colored so it reads as a cutout. */}
<div
data-slot="hero-stats"
className="absolute bottom-0 end-0 z-10 hidden items-end gap-8 rounded-ss-[18px] bg-background pb-2.5 pe-8 ps-7 pt-5 md:flex"
>
{STATS.map((stat) => (
<div key={stat.label}>
<div className="font-serif text-2xl font-medium leading-tight text-primary md:text-3xl">{stat.value}</div>
<div className="font-mono text-[10px] uppercase tracking-[0.12em] text-muted-foreground">
{stat.label}
</div>
</div>
))}
</div>
</HeroZoom>
</section>
);
};
export default Hero09;
Dependencies
shadcn registry
button
npm
motionlucide-react
Shipped