# Hero 45 - Hero with wide image and three feature columns

> A hero with outline badge, headline, a wide landscape image that crossfades between slides when each feature column is hovered, gradient veil, and three icon-led columns separated by vertical rules on desktop.

A shadcn/ui block from the [@shadcnblocks](https://shadcnblocks.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/shadcnblocks/hero45).

- Page, with a live preview: https://shadcn.exchange/r/shadcnblocks/hero45
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/shadcnblocks/hero45-light-337ddcf4ea2a5ba3.png
- Registry item: https://shadcnblocks.com/r/hero45.json
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://shadcnblocks.com/r/hero45.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: `cn`, `lucide-react`
- Registry items: `badge`, `separator`

## Files

### `block/hero45/hero45.tsx`

```tsx
"use client";

import { Braces, Cpu, Keyboard } from "lucide-react";
import React, { useState } from "react";
import { cn } from "cn";

import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator";

type HeroFeatureSliderImage = Image & {
  label?: string;
};
interface HeroFeatureSliderFeature {
  title: string;
  description: string;
  icon: ElementType<{ className?: string }>;
  color?: string;
  href?: string;
}
interface Image {
  src: string;
  alt: string;
  srcDark?: string;
}

interface HeroFeatureSliderProps {
  badge?: string;
  heading: string;
  features?: HeroFeatureSliderFeature[];
  images: [HeroFeatureSliderImage, ...HeroFeatureSliderImage[]];
  className?: string;
}

interface Hero45Props extends HeroFeatureSliderProps {}
type Props = Partial<Hero45Props>;

const defaultProps: Hero45Props = {
  badge: "Platform",
  heading: "Shadcn UI Components built for the modern stack.",
  features: [
    {
      title: "Composable patterns",
      description:
        "Ship faster with structured sections and consistent spacing.",
      icon: Braces,
    },
    {
      title: "Design tokens",
      description:
        "Theme and scale colors, type, and radii from a single coherent system.",
      icon: Cpu,
    },
    {
      title: "Accessible defaults",
      description:
        "Keyboard and screen-reader friendly building blocks out of the box.",
      icon: Keyboard,
    },
  ],
  images: [
    {
      src: "https://cdn.shadcnblocks.com/shadcnblocks/image-set/modern/saas-hero/saas-hero-2-16x9.png",
      srcDark: "https://cdn.shadcnblocks.com/shadcnblocks/image-set/modern/saas-hero/saas-hero-2-16x9-dark.png",
      alt: "Product preview",
      label: "Overview",
    },
    {
      src: "https://cdn.shadcnblocks.com/shadcnblocks/image-set/modern/saas-hero/saas-hero-3-16x9.png",
      srcDark: "https://cdn.shadcnblocks.com/shadcnblocks/image-set/modern/saas-hero/saas-hero-3-16x9-dark.png",
      alt: "Product detail",
      label: "Workflow",
    },
    {
      src: "https://cdn.shadcnblocks.com/shadcnblocks/image-set/modern/saas-hero/saas-hero-4-16x9.png",
      srcDark: "https://cdn.shadcnblocks.com/shadcnblocks/image-set/modern/saas-hero/saas-hero-4-16x9-dark.png",
      alt: "Product context",
      label: "Insights",
    },
  ],
};

/** Horizontal feature row; three columns in default layout. */
const MAX_FEATURES = 3;

const Hero45 = (props: Props) => {
  const { badge, heading, features, images, className } = {
    ...defaultProps,
    ...props,
  };

  const visibleFeatures = (features ?? []).slice(0, MAX_FEATURES);
  const featureCount = visibleFeatures.length;
  const [activeImageIndex, setActiveImageIndex] = useState(0);

  return (
    <section className={cn("py-32", className)}>
      <div className="container mx-auto overflow-hidden">
        <div className="mb-20 flex flex-col items-center gap-6 text-center">
          <Badge variant="outline">{badge}</Badge>
          <h1 className="max-w-3xl text-4xl font-semibold tracking-tight text-pretty lg:text-5xl">
            {heading}
          </h1>
        </div>
        <div className="relative mx-auto max-w-5xl">
          <div className="relative aspect-video max-h-[500px] w-full">
            <div className="absolute inset-0 overflow-hidden rounded-xl">
              {Array.from({ length: Math.max(featureCount, 1) }, (_, i) => {
                const heroImage = images[Math.min(i, images.length - 1)];
                const isActive = activeImageIndex === i;
                return (
                  <div
                    key={`${heroImage.src}-${i}`}
                    className={cn(
                      "absolute inset-0 transition-opacity duration-500 ease-out",
                      isActive
                        ? "z-10 opacity-100"
                        : "pointer-events-none z-0 opacity-0",
                    )}
                    aria-hidden={!isActive}
                  >
                    {heroImage.srcDark ? (
                      <>
                        <img
                          src={heroImage.src}
                          alt={heroImage.alt}
                          className="absolute inset-0 size-full rounded-xl border border-border object-cover object-top dark:hidden"
                        />
                        <img
                          src={heroImage.srcDark}
                          alt={heroImage.alt}
                          className="absolute inset-0 hidden size-full rounded-xl border border-border object-cover object-top dark:block"
                        />
                      </>
                    ) : (
                      <img
                        src={heroImage.src}
                        alt={heroImage.alt}
                        className="absolute inset-0 size-full rounded-xl border border-border object-cover object-top"
                      />
                    )}
                  </div>
                );
              })}
            </div>
            <div className="pointer-events-none absolute inset-0 z-20 rounded-xl bg-linear-to-t from-background via-transparent to-transparent" />
            <div className="pointer-events-none absolute -top-28 -right-28 -z-10 aspect-video h-72 w-96 mask-[radial-gradient(ellipse_50%_50%_at_50%_50%,#000_20%,transparent_100%)] bg-size-[12px_12px] opacity-40 sm:bg-[radial-gradient(hsl(var(--muted-foreground))_1px,transparent_1px)]" />
            <div className="pointer-events-none absolute -top-28 -left-28 -z-10 aspect-video h-72 w-96 mask-[radial-gradient(ellipse_50%_50%_at_50%_50%,#000_20%,transparent_100%)] bg-size-[12px_12px] opacity-40 sm:bg-[radial-gradient(hsl(var(--muted-foreground))_1px,transparent_1px)]" />
          </div>
        </div>
        <div
          className="mx-auto mt-10 flex max-w-5xl flex-col md:flex-row"
          onMouseLeave={() => setActiveImageIndex(0)}
        >
          {visibleFeatures.map((feature, index) => (
            <React.Fragment key={feature.title}>
              {index > 0 && (
                <Separator
                  orientation="vertical"
                  className="mx-6 hidden h-auto w-0.5 bg-linear-to-b from-muted via-transparent to-muted md:block"
                />
              )}
              <div
                className="flex grow basis-0 cursor-default flex-col rounded-md bg-background p-4 transition-colors hover:bg-muted/40"
                onMouseEnter={() => setActiveImageIndex(index)}
              >
                <div className="mb-6 flex size-10 items-center justify-center rounded-full bg-background drop-shadow-lg">
                  <feature.icon className="h-auto w-5" />
                </div>
                <h3 className="mb-2 font-semibold tracking-tight text-foreground">
                  {feature.title}
                </h3>
                <p className="text-sm text-balance text-muted-foreground">
                  {feature.description}
                </p>
              </div>
            </React.Fragment>
          ))}
        </div>
      </div>
    </section>
  );
};

export { Hero45 };
```
