# Page Hero

> Full-bleed gradient hero band — badge, title, highlight, description, stats row, and a reveal-on-mount animation.

A shadcn/ui block from the [@ilinxa](https://ui.ilinxa.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/ilinxa/page-hero).

- Page, with a live preview: https://shadcn.exchange/r/ilinxa/page-hero
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/ilinxa/page-hero-light-266855118b33c037.png
- Registry item: https://ui.ilinxa.com/r/page-hero.json
- Categories: marketing, news
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://ui.ilinxa.com/r/page-hero.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `lucide-react`

## Files

### `src/registry/components/marketing/page-hero/page-hero.tsx`

```tsx
"use client";

import { memo, useId } from "react";
import { cn } from "@/lib/utils";
import type {
  PageHeroDensity,
  PageHeroHeadingLevel,
  PageHeroProps,
} from "./types";

const DENSITY_CLASS: Record<PageHeroDensity, string> = {
  compact: "min-h-[40vh]",
  default: "min-h-[70vh]",
  full: "min-h-screen",
};

/**
 * PageHero — full-bleed gradient hero band with badge / title /
 * highlight / description / children slot. Reveal animation uses the
 * existing pro-ui `reveal-up` keyframe (no framer-motion peer dep) with
 * 60ms staggered delays per the project's Motion mandate.
 *
 * Pair with the exported `HeroStats` sub-component for the typical
 * icon-circle + value + label triplet inside `children`.
 */
function PageHeroImpl(props: PageHeroProps) {
  const {
    title,
    titleSlot,
    titleHighlight,
    badge,
    badgeIcon: BadgeIcon,
    description,
    children,
    density = "default",
    headingAs = "h1",
    disableReveal = false,
    className,
  } = props;

  const generatedId = useId();
  const titleId = `${generatedId}-title`;
  const HeadingTag = headingAs as PageHeroHeadingLevel;

  const revealClass = disableReveal ? "" : "reveal-up";

  return (
    <section
      aria-labelledby={titleId}
      className={cn(
        "relative flex items-center justify-center overflow-hidden",
        DENSITY_CLASS[density],
        className,
      )}
    >
      {/* Gradient background + subtle noise overlay */}
      <div className="absolute inset-0">
        <div className="absolute inset-0 bg-linear-to-br from-primary via-primary/95 to-primary/80" />
        <div
          aria-hidden="true"
          className="absolute inset-0 opacity-50 bg-[url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHZpZXdCb3g9IjAgMCA2MCA2MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxnIGZpbGw9IiNmZmZmZmYiIGZpbGwtb3BhY2l0eT0iMC4wMyI+PHBhdGggZD0iTTM2IDM0djItSDI0di0yaDEyek0zNiAyNHYySDI0di0yaDEyeiIvPjwvZz48L2c+PC9zdmc+')]"
        />
      </div>

      {/* Content stack */}
      <div className="container relative z-10 mx-auto max-w-4xl px-4 py-24 text-center">
        {badge ? (
          <span
            className={cn(
              "mb-6 inline-flex items-center gap-2 rounded-full bg-primary-foreground/10 px-4 py-2 text-sm font-medium text-primary-foreground",
              revealClass,
            )}
            style={disableReveal ? undefined : { animationDelay: "0ms" }}
          >
            {BadgeIcon ? <BadgeIcon className="h-4 w-4" aria-hidden="true" /> : null}
            {badge}
          </span>
        ) : null}

        {titleSlot ? (
          <div
            className={revealClass}
            style={disableReveal ? undefined : { animationDelay: "60ms" }}
          >
            {titleSlot}
          </div>
        ) : (
          <HeadingTag
            id={titleId}
            className={cn(
              "mb-6 font-bold text-4xl text-primary-foreground md:text-5xl lg:text-6xl",
              revealClass,
            )}
            style={disableReveal ? undefined : { animationDelay: "60ms" }}
          >
            {title}
            {titleHighlight ? (
              <span className="block italic text-primary-foreground">{titleHighlight}</span>
            ) : null}
          </HeadingTag>
        )}

        {description ? (
          <p
            className={cn(
              "mx-auto max-w-3xl text-lg text-primary-foreground/80 md:text-xl",
              revealClass,
            )}
            style={disableReveal ? undefined : { animationDelay: "120ms" }}
          >
            {description}
          </p>
        ) : null}

        {children ? (
          <div
            className={cn("mt-10", revealClass)}
            style={disableReveal ? undefined : { animationDelay: "180ms" }}
          >
            {children}
          </div>
        ) : null}
      </div>

      {/* Bottom wave — fills with the page background so the section transitions cleanly */}
      <div aria-hidden="true" className="absolute -bottom-px left-0 right-0">
        <svg
          viewBox="0 0 1440 120"
          fill="none"
          xmlns="http://www.w3.org/2000/svg"
          className="fill-background"
        >
          <path d="M0 120L60 110C120 100 240 80 360 70C480 60 600 60 720 65C840 70 960 80 1080 85C1200 90 1320 90 1380 90L1440 90V120H1380C1320 120 1200 120 1080 120C960 120 840 120 720 120C600 120 480 120 360 120C240 120 120 120 60 120H0Z" />
        </svg>
      </div>
    </section>
  );
}

export const PageHero = memo(PageHeroImpl);
PageHero.displayName = "PageHero";

export default PageHero;
export { HeroStats } from "./parts/hero-stats";
```

### `src/registry/components/marketing/page-hero/index.ts`

```ts
export { PageHero, HeroStats, default } from "./page-hero";
export type {
  HeroStat,
  HeroStatsProps,
  PageHeroDensity,
  PageHeroHeadingLevel,
  PageHeroProps,
} from "./types";
```

### `src/registry/components/marketing/page-hero/types.ts`

```ts
import type { ComponentType, ReactNode } from "react";

export type PageHeroDensity = "compact" | "default" | "full";

export type PageHeroHeadingLevel = "h1" | "h2" | "h3";

export interface HeroStat {
  /** Optional Lucide-style icon. */
  icon?: ComponentType<{ className?: string }>;
  /** Bold value text (e.g. "500+", "Daily"). */
  value: string;
  /** Small label below the value (e.g. "Articles", "Updates"). */
  label: string;
}

export interface HeroStatsProps {
  stats: HeroStat[];
  className?: string;
}

export interface PageHeroProps {
  /** Title is required. */
  title: string;
  /** Override `title` with a custom node (e.g. mixed bold + colors). */
  titleSlot?: ReactNode;
  /** Optional accent-colored highlight rendered as a `<span class="block">` below the title. */
  titleHighlight?: string;
  /** Optional badge text rendered above the title. */
  badge?: string;
  /** Optional Lucide-style icon for the badge. */
  badgeIcon?: ComponentType<{ className?: string }>;
  /** Optional description paragraph rendered below the title. */
  description?: string;
  /** Optional content slot rendered below the description (stats / CTAs / search / anything). */
  children?: ReactNode;

  /** Section minimum height. Default: 'default' (70vh). */
  density?: PageHeroDensity;
  /** Heading semantic level. Default: 'h1'. */
  headingAs?: PageHeroHeadingLevel;
  /** Disable the entrance animation entirely. Default: false. */
  disableReveal?: boolean;

  /** Override classes for the root <section>. */
  className?: string;
}
```

### `src/registry/components/marketing/page-hero/parts/hero-stats.tsx`

```tsx
import { cn } from "@/lib/utils";
import type { HeroStatsProps } from "../types";

/**
 * 3-stat row pattern from the kasder NewsHero — icon-circle + bold value
 * + small label, repeated horizontally. Designed to live inside the hero's
 * `children` slot; inherits primary-foreground (near-black) text from the
 * lime gradient background per the design-system mandate.
 */
export function HeroStats({ stats, className }: HeroStatsProps) {
  return (
    <div className={cn("flex flex-wrap justify-center gap-8", className)}>
      {stats.map((stat, index) => {
        const Icon = stat.icon;
        return (
          <div
            key={index}
            className="flex items-center gap-3 text-primary-foreground/90"
          >
            {Icon ? (
              <div className="flex h-12 w-12 items-center justify-center rounded-full bg-primary-foreground/10">
                <Icon className="h-5 w-5" />
              </div>
            ) : null}
            <div className="text-left">
              <div className="text-2xl font-bold">{stat.value}</div>
              <div className="text-sm text-primary-foreground/70">{stat.label}</div>
            </div>
          </div>
        );
      })}
    </div>
  );
}
```
