# Services Image Grid

> An intro with a heading, a short pitch, and one outline action beside five image cards that fade when you point at them

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/services-image-grid-01).

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

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/cortexcn/services-image-grid-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: `motion`
- Registry items: `https://ui.cortexcn.dev/r/utils.json`, `button`, `card`

## Files

### `registry/blocks/services-image-grid-01/components/services-image-grid.tsx`

```tsx
"use client";

import { motion } from "motion/react";

import { buttonVariants } from "@/components/ui/button";
import { Card, CardAction, CardHeader, CardTitle } from "@/components/ui/card";
import { cn } from "@/lib/utils";

// Cortexcn item "services-image-grid-01", revision 11ba78f5975e, MIT license.
// https://ui.cortexcn.dev/r/v/services-image-grid-01/11ba78f5975e.json

interface ServicesAction {
  label: string;
  href: string;
}

export interface ServiceItem {
  title: string;
  href: string;
  image: { src: string; alt: string };
}

export interface ServicesImageGridProps {
  heading?: string;
  description?: string;
  action?: ServicesAction;
  /** The first two are tall cards; the rest sit three to a row beneath them. */
  services?: ServiceItem[];
  className?: string;
}

/** The flower paintings from the Cortexcn site. */
const ART_BASE = "https://www.cortexcn.dev/images/ui-home";

const DEFAULT_SERVICES: ServiceItem[] = [
  {
    title: "Landing pages",
    href: "#",
    image: {
      src: `${ART_BASE}/watercolor-wildflowers.webp`,
      alt: "A watercolor of tall wildflowers in soft blue and gold",
    },
  },
  {
    title: "Websites",
    href: "#",
    image: {
      src: `${ART_BASE}/wildflower-meadow.webp`,
      alt: "A painted meadow of wildflowers bending in the wind",
    },
  },
  {
    title: "Web applications",
    href: "#",
    image: {
      src: `${ART_BASE}/blue-white-flowers.webp`,
      alt: "A painting of white flowers against a pale blue sky",
    },
  },
  {
    title: "E-commerce stores",
    href: "#",
    image: {
      src: `${ART_BASE}/pastel-daisies.webp`,
      alt: "A painting of daisies in pastel greens and creams",
    },
  },
  {
    title: "AI pipelines",
    href: "#",
    image: {
      src: `${ART_BASE}/white-blossoms.webp`,
      alt: "A painting of white blossoms on a leafy branch",
    },
  },
];

/** How many services lead the grid as tall cards. */
const FEATURED_COUNT = 2;

function ArrowUpRightIcon({ className }: { className?: string }) {
  return (
    <svg
      aria-hidden="true"
      className={cn("size-4", className)}
      fill="none"
      stroke="currentColor"
      strokeLinecap="round"
      strokeLinejoin="round"
      strokeWidth="2"
      viewBox="0 0 24 24"
      xmlns="http://www.w3.org/2000/svg"
    >
      <path d="M7 7h10v10M7 17 17 7" />
    </svg>
  );
}

function ServiceCard({
  service,
  featured,
}: {
  service: ServiceItem;
  featured: boolean;
}) {
  return (
    <motion.a
      className="group block rounded-xl outline-none focus-visible:ring-3 focus-visible:ring-ring/50"
      href={service.href}
      transition={{ duration: 0.2, ease: "easeOut" }}
      whileHover={{ opacity: 0.8 }}
    >
      <Card
        className={cn("relative py-0", featured ? "aspect-3/4" : "aspect-4/3")}
      >
        {/* biome-ignore lint/performance/noImgElement: framework-agnostic registry block */}
        <img
          alt={service.image.alt}
          className="absolute inset-0 size-full object-cover"
          decoding="async"
          height={featured ? 1067 : 450}
          loading="lazy"
          src={service.image.src}
          width={featured ? 800 : 600}
        />
        {/* Keeps the title readable on light photos. */}
        <div
          aria-hidden="true"
          className="absolute inset-x-0 top-0 h-1/2 bg-linear-to-b from-black/60 to-transparent"
        />
        <CardHeader className={cn("relative", featured ? "p-6" : "p-4")}>
          <CardTitle
            className={cn(
              "pr-4 font-semibold text-white",
              featured ? "text-base" : "text-sm",
            )}
          >
            {service.title}
          </CardTitle>
          <CardAction>
            <ArrowUpRightIcon
              className={cn(
                "text-white transition-transform duration-200 ease-out motion-safe:group-hover:scale-110",
                featured ? "size-6" : "size-5",
              )}
            />
          </CardAction>
        </CardHeader>
      </Card>
    </motion.a>
  );
}

export function ServicesImageGrid({
  heading = "What we build",
  description = "Landing pages, websites, and full web apps, designed and built by one small team. Tell us what you need and get a written quote.",
  action = { label: "View all services", href: "#" },
  services = DEFAULT_SERVICES,
  className,
}: ServicesImageGridProps) {
  return (
    <section className={cn("px-6 py-24 sm:py-32", className)}>
      <div className="mx-auto grid max-w-6xl gap-10 lg:grid-cols-3">
        <div className="flex flex-col justify-between gap-8">
          <div className="flex flex-col gap-4">
            <h2 className="text-balance font-semibold text-4xl tracking-tight md:text-5xl">
              {heading}
            </h2>
            <p className="max-w-xs text-muted-foreground leading-relaxed">
              {description}
            </p>
          </div>
          <a
            className={cn(
              buttonVariants({ variant: "outline" }),
              "w-fit gap-1.5 px-4",
            )}
            href={action.href}
          >
            {action.label}
            <ArrowUpRightIcon />
          </a>
        </div>

        <ul className="grid gap-4 sm:grid-cols-6 lg:col-span-2">
          {services.map((service, index) => {
            const featured = index < FEATURED_COUNT;
            return (
              <li
                className={featured ? "sm:col-span-3" : "sm:col-span-2"}
                key={service.title}
              >
                <ServiceCard featured={featured} service={service} />
              </li>
            );
          })}
        </ul>
      </div>
    </section>
  );
}
```
