# Hero Dashboard Preview

> A serif headline, a short pitch, and one call to action over a framed product dashboard on a soft photo wash

A shadcn/ui block from the [@cortexcn](https://www.cortexcn.dev/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/cortexcn/hero-dashboard-01).

- Page, with a live preview: https://shadcn.exchange/r/cortexcn/hero-dashboard-01
- Screenshot: https://shadcn.exchange/shot/cortexcn/hero-dashboard-01/light.png
- Registry item: https://ui.cortexcn.dev/r/hero-dashboard-01.json
- Listed: 2026-11-05, source last changed 2026-11-05

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/cortexcn/hero-dashboard-01.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: `class-variance-authority`, `lucide-react`, `motion`
- Registry items: `https://ui.cortexcn.dev/r/utils.json`, `avatar`, `badge`, `button`, `card`, `separator`

## Files

### `registry/blocks/hero-dashboard-01/components/hero-dashboard.tsx`

```tsx
"use client";

import { motion, useReducedMotion, type Variants } from "motion/react";
import type { ReactNode } from "react";

import { cn } from "@/lib/utils";

import { Cta, type CtaProps } from "./hero-cta";
import { DashboardDemo } from "./hero-dashboard-demo";

// Cortexcn item "hero-dashboard-01", revision 27fff60c5fe5, MIT license.
// https://ui.cortexcn.dev/r/v/hero-dashboard-01/27fff60c5fe5.json

export interface HeroDashboardProps {
  title?: string;
  /** Starts on its own line under the title. */
  titleLine2?: string;
  description?: string;
  /** Photo behind the product preview, softened by a gradient. */
  washImage?: string;
  animation?: "none" | "subtle";
  primaryCTA?: CtaProps;
  variant?: "standard" | "compact";
}

const DEFAULTS = {
  title: "Every metric that matters,",
  titleLine2: "in one clear view.",
  description:
    "Track revenue, users, and activity in real time, with no setup and no spreadsheets.",
  // Sample photo from Unsplash (free to use under the Unsplash License).
  washImage:
    "https://images.unsplash.com/photo-1578301978018-3005759f48f7?q=80&w=1144&auto=format&fit=crop",
  primaryCTA: {
    ctaEnabled: true,
    text: "Start free",
    link: "",
    variant: "default",
    size: "default",
  },
} satisfies HeroDashboardProps;

const variantStyles = {
  standard: {
    section: "py-20 sm:py-28",
    title: "text-3xl sm:text-4xl md:text-5xl",
    description: "max-w-md text-sm sm:text-base",
    header: "gap-5",
    content: "gap-14 sm:gap-20",
  },
  compact: {
    section: "py-14 sm:py-20",
    title: "text-2xl sm:text-3xl md:text-4xl",
    description: "max-w-sm text-sm",
    header: "gap-4",
    content: "gap-10 sm:gap-14",
  },
} as const;

const container: Variants = {
  hidden: {},
  visible: { transition: { staggerChildren: 0.1, delayChildren: 0.05 } },
};

const item: Variants = {
  hidden: { opacity: 0, y: 12, filter: "blur(6px)" },
  visible: {
    opacity: 1,
    y: 0,
    filter: "blur(0px)",
    transition: { duration: 0.5, ease: [0.22, 1, 0.36, 1] },
  },
};

const mediaItem: Variants = {
  hidden: { opacity: 0, y: 24, filter: "blur(8px)" },
  visible: {
    opacity: 1,
    y: 0,
    filter: "blur(0px)",
    transition: { duration: 0.6, ease: [0.22, 1, 0.36, 1] },
  },
};

function Reveal({
  active,
  variants,
  className,
  children,
}: Readonly<{
  active: boolean;
  variants?: Variants;
  className?: string;
  children: ReactNode;
}>) {
  if (!active) {
    return <div className={className}>{children}</div>;
  }

  return (
    <motion.div className={className} variants={variants ?? item}>
      {children}
    </motion.div>
  );
}

/**
 * A serif headline, a short pitch, and one call to action over a framed
 * product dashboard on a soft photo wash.
 */
export function HeroDashboard({
  title = DEFAULTS.title,
  titleLine2 = DEFAULTS.titleLine2,
  description = DEFAULTS.description,
  washImage = DEFAULTS.washImage,
  animation = "subtle",
  primaryCTA = DEFAULTS.primaryCTA,
  variant = "standard",
}: Readonly<HeroDashboardProps>) {
  const reduce = useReducedMotion();
  const animate = animation === "subtle" && !reduce;
  const vs = variantStyles[variant];

  return (
    <section className="relative isolate w-full overflow-hidden bg-background">
      <motion.div
        className={cn(
          "relative z-10 mx-auto flex max-w-6xl flex-col px-6",
          vs.section,
          vs.content,
        )}
        initial={animate ? "hidden" : false}
        variants={animate ? container : undefined}
        viewport={{ once: true, margin: "-80px" }}
        whileInView={animate ? "visible" : undefined}
      >
        <Reveal
          active={animate}
          className={cn("flex max-w-2xl flex-col items-start", vs.header)}
        >
          {title && (
            <h1
              className={cn(
                "text-balance font-normal font-serif text-foreground tracking-tight",
                vs.title,
              )}
            >
              {title}
              {titleLine2 && (
                <>
                  <br />
                  {titleLine2}
                </>
              )}
            </h1>
          )}
          {description && (
            <p
              className={cn(
                "text-balance text-muted-foreground",
                vs.description,
              )}
            >
              {description}
            </p>
          )}
          <Cta cta={primaryCTA} />
        </Reveal>

        <Reveal active={animate} className="w-full" variants={mediaItem}>
          <div className="relative w-full overflow-hidden rounded-md outline outline-black/10 dark:outline-white/10">
            {washImage && (
              // biome-ignore lint/performance/noImgElement: framework-agnostic registry block
              <img
                alt=""
                aria-hidden="true"
                className="absolute inset-0 size-full object-cover"
                decoding="async"
                height={760}
                src={washImage}
                width={1144}
              />
            )}
            <div className="absolute inset-0 bg-linear-to-b from-background/30 via-background/10 to-background/40" />
            <div className="relative flex items-center justify-center px-6 py-12 sm:px-12 sm:py-16">
              <DashboardDemo />
            </div>
          </div>
        </Reveal>
      </motion.div>
    </section>
  );
}
```

### `registry/blocks/hero-dashboard-01/components/hero-dashboard-demo.tsx`

```tsx
import {
  Activity,
  BarChart3,
  Bell,
  LayoutGrid,
  Search,
  Settings,
  Users,
} from "lucide-react";

import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import { cn } from "@/lib/utils";

// Cortexcn item "hero-dashboard-01", revision 27fff60c5fe5, MIT license.
// https://ui.cortexcn.dev/r/v/hero-dashboard-01/27fff60c5fe5.json

const nav = [
  { icon: LayoutGrid, label: "Overview", active: true },
  { icon: BarChart3, label: "Reports", active: false },
  { icon: Users, label: "Customers", active: false },
  { icon: Activity, label: "Activity", active: false },
  { icon: Settings, label: "Settings", active: false },
];

const stats = [
  { label: "Revenue", value: "$48.2k", trend: "+12.4%", up: true },
  { label: "Active users", value: "2,318", trend: "+4.1%", up: true },
  { label: "Conversion", value: "3.6%", trend: "+0.8%", up: true },
  { label: "Churn", value: "1.4%", trend: "-2.0%", up: false },
];

const bars = [
  { month: "Jan", value: 42 },
  { month: "Feb", value: 58 },
  { month: "Mar", value: 36 },
  { month: "Apr", value: 64 },
  { month: "May", value: 48 },
  { month: "Jun", value: 72 },
  { month: "Jul", value: 55 },
  { month: "Aug", value: 80 },
  { month: "Sep", value: 61 },
  { month: "Oct", value: 88 },
  { month: "Nov", value: 70 },
  { month: "Dec", value: 94 },
];

const activity = [
  {
    src: "https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=64&q=80",
    name: "James Doe",
    action: "upgraded to Pro",
    time: "2m",
  },
  {
    src: "https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=64&q=80",
    name: "Sara Lin",
    action: "invited 3 members",
    time: "18m",
  },
  {
    src: "https://images.unsplash.com/photo-1633332755192-727a05c4013d?w=64&q=80",
    name: "Marcus Vale",
    action: "connected Stripe",
    time: "1h",
  },
];

/** A sample analytics app in a browser window, for the hero's media slot. */
export function DashboardDemo() {
  return (
    <div className="relative w-full max-w-4xl">
      <Card className="w-full gap-0 overflow-hidden bg-card p-0 shadow-2xl outline outline-black/10 dark:outline-white/10">
        {/* Window chrome */}
        <div className="flex items-center gap-2 border-b px-3 py-2.5">
          <div className="flex shrink-0 gap-1.5">
            <span className="size-2.5 rounded-full bg-red-400/70" />
            <span className="size-2.5 rounded-full bg-amber-400/70" />
            <span className="size-2.5 rounded-full bg-emerald-400/70" />
          </div>
          <div className="mx-auto flex h-6 w-full min-w-0 max-w-56 items-center gap-1.5 rounded-md bg-muted px-2 text-[10px] text-muted-foreground">
            <Search className="size-3 shrink-0" />
            <span className="truncate">app.acme.com/overview</span>
          </div>
          <Avatar className="size-6 shrink-0">
            <AvatarImage
              alt=""
              src="https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=64&q=80"
            />
            <AvatarFallback className="text-[9px]">JD</AvatarFallback>
          </Avatar>
        </div>

        <div className="flex">
          {/* Sidebar */}
          <div className="hidden w-44 shrink-0 flex-col gap-1 border-r p-3 sm:flex">
            <div className="mb-2 flex items-center gap-2 px-1">
              <div className="size-6 rounded-lg bg-primary/85" />
              <span className="font-semibold text-sm tracking-tight">Acme</span>
            </div>
            {nav.map(({ icon: Icon, label, active }) => (
              <div
                className={cn(
                  "flex items-center gap-2 rounded-lg px-2 py-1.5 text-xs",
                  active
                    ? "bg-muted font-medium text-foreground"
                    : "text-muted-foreground",
                )}
                key={label}
              >
                <Icon className="size-3.5 shrink-0" />
                <span className="truncate">{label}</span>
              </div>
            ))}
          </div>

          {/* Main */}
          <div className="flex min-w-0 flex-1 flex-col gap-3 p-3 sm:p-4">
            {/* Header */}
            <div className="flex items-center justify-between gap-2">
              <div className="flex min-w-0 flex-col gap-0.5">
                <span className="truncate font-semibold text-sm tracking-tight">
                  Overview
                </span>
                <span className="truncate text-[10px] text-muted-foreground">
                  Last 30 days · updated 2m ago
                </span>
              </div>
              <div className="flex shrink-0 items-center gap-1.5">
                {/* Hidden on phones so the page title keeps its room. */}
                <Button className="max-sm:hidden" size="xs" variant="outline">
                  Export
                </Button>
                <Button size="xs">New report</Button>
              </div>
            </div>

            {/* KPI row */}
            <div className="grid grid-cols-2 gap-2 lg:grid-cols-4">
              {stats.map((stat) => (
                <div
                  className="min-w-0 rounded-lg border p-2.5"
                  key={stat.label}
                >
                  <span className="block truncate text-[9px] text-muted-foreground">
                    {stat.label}
                  </span>
                  <div className="mt-1 flex flex-wrap items-baseline justify-between gap-x-1">
                    <span className="font-semibold text-sm tabular-nums">
                      {stat.value}
                    </span>
                    <span
                      className={cn(
                        "font-medium text-[9px] tabular-nums",
                        stat.up
                          ? "text-emerald-600 dark:text-emerald-400"
                          : "text-red-500 dark:text-red-400",
                      )}
                    >
                      {stat.trend}
                    </span>
                  </div>
                </div>
              ))}
            </div>

            {/* Chart */}
            <div className="rounded-lg border p-3">
              <div className="flex items-center justify-between gap-2">
                <div className="flex min-w-0 flex-col gap-0.5">
                  <span className="truncate font-medium text-xs">
                    Monthly revenue
                  </span>
                  <span className="truncate text-[9px] text-muted-foreground">
                    $48,240 total
                  </span>
                </div>
                <span className="flex shrink-0 items-center gap-1 text-[9px] text-muted-foreground">
                  <span className="size-1.5 rounded-full bg-primary/80" />
                  This year
                </span>
              </div>

              <div className="mt-3 flex h-20 items-end gap-1.5">
                {bars.map((bar) => (
                  <div
                    className="min-h-0.5 flex-1 rounded-sm bg-primary/70"
                    key={bar.month}
                    style={{ height: `${bar.value}%` }}
                    title={`${bar.month}: ${bar.value}`}
                  />
                ))}
              </div>
            </div>

            {/* Recent activity */}
            <div className="rounded-lg border p-3">
              <div className="flex items-center justify-between">
                <span className="font-medium text-xs">Recent activity</span>
                <Badge className="text-[9px]" variant="secondary">
                  Live
                </Badge>
              </div>
              <div className="mt-2 flex flex-col">
                {activity.map((entry, index) => (
                  <div key={entry.name}>
                    {index > 0 && <Separator className="my-2" />}
                    <div className="flex items-center gap-2">
                      <Avatar className="size-6 shrink-0">
                        <AvatarImage alt="" src={entry.src} />
                        <AvatarFallback className="text-[9px]">
                          {entry.name[0]}
                        </AvatarFallback>
                      </Avatar>
                      <span className="min-w-0 flex-1 truncate text-[11px]">
                        <span className="font-medium">{entry.name}</span>
                        <span className="text-muted-foreground">
                          {" "}
                          {entry.action}
                        </span>
                      </span>
                      <span className="shrink-0 text-[9px] text-muted-foreground tabular-nums">
                        {entry.time}
                      </span>
                    </div>
                  </div>
                ))}
              </div>
            </div>
          </div>
        </div>
      </Card>

      {/* Floating notification */}
      <div className="absolute right-1 bottom-1 max-w-[calc(100%-0.5rem)] sm:-right-4 sm:-bottom-4">
        <Card className="flex-row items-center gap-2.5 p-2.5 shadow-lg">
          <div className="relative flex size-8 shrink-0 items-center justify-center rounded-lg bg-primary/15 text-primary">
            <Bell className="size-4" />
            <span className="absolute -top-0.5 -right-0.5 size-2 rounded-full border-2 border-card bg-emerald-500" />
          </div>
          <div className="flex min-w-0 flex-col">
            <span className="truncate font-medium text-xs leading-tight">
              Payment received
            </span>
            <span className="truncate text-[10px] text-muted-foreground leading-tight">
              $1,290 from Acme Inc.
            </span>
          </div>
          <Badge className="ml-1 shrink-0 text-[10px]" variant="secondary">
            +$1.2k
          </Badge>
        </Card>
      </div>
    </div>
  );
}
```

### `registry/blocks/hero-dashboard-01/components/hero-cta.tsx`

```tsx
import { cva, type VariantProps } from "class-variance-authority";

import { Button, buttonVariants } from "@/components/ui/button";
import { cn } from "@/lib/utils";

// Cortexcn item "hero-dashboard-01", revision 27fff60c5fe5, MIT license.
// https://ui.cortexcn.dev/r/v/hero-dashboard-01/27fff60c5fe5.json

type ButtonVariants = VariantProps<typeof buttonVariants>;

export type ButtonVariant = NonNullable<ButtonVariants["variant"]>;

export interface CtaProps {
  ctaEnabled?: boolean;
  text: string;
  /** Leave empty to render a button instead of a link. */
  link: string;
  variant?: ButtonVariant;
  size?: "default" | "sm" | "lg";
}

const ctaVariants = cva("w-fit rounded-full px-4", {
  variants: {
    invert: {
      true: "border-transparent! bg-white! text-zinc-900! hover:bg-zinc-100!",
      false: "",
    },
  },
  defaultVariants: {
    invert: false,
  },
});

export function Cta({
  cta,
  className,
  invert,
}: Readonly<{ cta?: CtaProps; className?: string; invert?: boolean }>) {
  if (!cta?.ctaEnabled) {
    return null;
  }

  const variant = cta.variant ?? (invert ? "secondary" : "default");

  if (!cta.link) {
    return (
      <Button
        aria-label={cta.text}
        className={cn(ctaVariants({ invert }), className)}
        size={cta.size ?? "default"}
        type="button"
        variant={variant}
      >
        {cta.text}
      </Button>
    );
  }

  return (
    // No aria-label here, so screen readers also hear "(opens in new tab)".
    <a
      className={cn(
        buttonVariants({
          variant,
          size: cta.size ?? "default",
          className: cn(ctaVariants({ invert }), className),
        }),
      )}
      href={cta.link}
      rel="noopener noreferrer"
      target="_blank"
    >
      {cta.text}
      <span className="sr-only">{`${cta.text} (opens in new tab)`}</span>
    </a>
  );
}
```
