# Services Grid
*URL: https://www.grettech.com/components/services-grid*

A responsive grid displaying agency services with icons, hover effects, and a unified aesthetic.

- Responsive grid, up to three columns
- Optional double-width cards (colSpan)
- Staggered entrance animation
- Works with your framework's Link component (linkAs)

## Install with the shadcn CLI
```bash
npx shadcn@latest add https://www.grettech.com/api/registry/services-grid
```
With its demo: `npx shadcn@latest add https://www.grettech.com/api/registry/services-grid-demo`

**Dependencies:** npm: clsx, tailwind-merge, motion, lucide-react

## Manual installation
**Step 1: Install dependencies**
Install the packages this component uses and the shadcn/ui components it builds on. It also needs the `cn` helper in `lib/utils.ts`, which `npx shadcn@latest init` adds.

```bash
npm install clsx tailwind-merge motion lucide-react
```

**Step 2: Copy the source code**
Add these files to your project components directory.

File: `components/ui/services-grid.tsx`
```tsx
"use client"

import * as React from "react"
import { cn } from "@/lib/utils"
import { motion } from "motion/react"

export interface ServiceItem {
  title: string
  description: string
  icon: React.ReactNode
  link?: string
  colSpan?: number
}

export interface ServicesGridProps extends React.HTMLAttributes<HTMLDivElement> {
  title?: string
  subtitle?: string
  services: ServiceItem[]
  linkAs?: React.ElementType
}

function ServiceCard({
  item,
  LinkTag,
}: {
  item: ServiceItem
  LinkTag: React.ElementType
}) {
  const wide = item.colSpan === 2
  const body = (
    <motion.div
      whileHover={{ y: -4 }}
      transition={{ type: "spring", stiffness: 300, damping: 20 }}
      className={cn(
        "relative z-10 flex h-full flex-col overflow-hidden rounded-3xl border border-border/60 bg-card p-6 shadow-sm transition-[border-color,box-shadow] duration-500 md:p-8",
        item.link &&
          "group-hover:border-primary/40 group-hover:shadow-[0_8px_30px_rgba(0,0,0,0.06)] dark:group-hover:shadow-[0_8px_30px_rgba(255,255,255,0.04)]"
      )}
    >
      {/* Soft wash that fades in on hover */}
      <div
        aria-hidden="true"
        className="absolute inset-0 -z-10 bg-linear-to-br from-primary/5 via-transparent to-transparent opacity-0 transition-opacity duration-500 group-hover:opacity-100"
      />

      <div className="w-fit rounded-2xl border border-primary/15 bg-primary/10 p-3.5 text-primary transition-colors duration-500 group-hover:bg-primary group-hover:text-primary-foreground [&_svg]:size-6">
        {item.icon}
      </div>

      <h3
        className={cn(
          "mt-5 font-semibold tracking-tight text-balance text-foreground",
          wide ? "text-2xl md:text-3xl" : "text-xl md:text-2xl"
        )}
      >
        {item.title}
      </h3>

      <p
        className={cn(
          "mt-3 text-pretty text-muted-foreground transition-colors duration-300 group-hover:text-foreground/80",
          wide ? "max-w-xl text-base md:text-lg" : "text-sm md:text-base"
        )}
      >
        {item.description}
      </p>
    </motion.div>
  )

  // Only real links are focusable; a card without one is plain content
  if (!item.link) return <div className="group h-full">{body}</div>

  return (
    <LinkTag
      href={item.link}
      className="group block h-full rounded-3xl outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
    >
      {body}
    </LinkTag>
  )
}

export const ServicesGrid = React.forwardRef<HTMLElement, ServicesGridProps>(
  (
    {
      title = "Our Services",
      subtitle = "We offer a wide range of web and app services",
      services,
      className,
      linkAs: LinkTag = "a",
      ...props
    },
    ref
  ) => {
    return (
      <section
        ref={ref}
        className={cn("relative bg-background py-24 md:py-32", className)}
        {...props}
      >
        <div className="mx-auto max-w-7xl px-4 md:px-8">
          <div className="relative mx-auto max-w-2xl text-center">
            <div className="relative z-10">
              <h2 className="mb-4 bg-linear-to-b from-foreground to-foreground/70 bg-clip-text text-4xl font-bold tracking-tight text-balance text-transparent md:text-5xl lg:text-6xl">
                {title}
              </h2>
              <p className="mx-auto max-w-2xl text-base text-pretty text-muted-foreground md:text-lg">
                {subtitle}
              </p>
            </div>
            <div
              aria-hidden="true"
              className="pointer-events-none absolute inset-0 mx-auto h-44 max-w-xs blur-[118px]"
              style={{
                background:
                  "linear-gradient(152.92deg, rgba(59, 130, 246, 0.2) 4.54%, rgba(37, 99, 235, 0.26) 34.2%, rgba(59, 130, 246, 0.1) 77.55%)",
              }}
            />
          </div>

          <div className="relative mt-12">
            <motion.ul
              initial="hidden"
              whileInView="visible"
              viewport={{ once: true, amount: 0.1 }}
              variants={{
                hidden: { opacity: 0 },
                visible: {
                  opacity: 1,
                  transition: { staggerChildren: 0.15, delayChildren: 0.2 },
                },
              }}
              className="grid auto-rows-fr gap-6 md:grid-cols-2 lg:grid-cols-3"
            >
              {services.map((item, idx: number) => (
                <motion.li
                  key={idx}
                  variants={{
                    hidden: { opacity: 0, y: 30, scale: 0.95 },
                    visible: {
                      opacity: 1,
                      y: 0,
                      scale: 1,
                      transition: {
                        type: "spring" as const,
                        stiffness: 100,
                        damping: 20,
                      },
                    },
                  }}
                  className={cn(
                    item.colSpan === 2 ? "md:col-span-2" : "col-span-1"
                  )}
                >
                  <ServiceCard item={item} LinkTag={LinkTag} />
                </motion.li>
              ))}
            </motion.ul>
          </div>
        </div>
      </section>
    )
  }
)
ServicesGrid.displayName = "ServicesGrid"

```

**Step 3: Framework Adjustments (Optional)**
If you are manually copying this code into a Vite or Remix project (without using the CLI), make sure to remove the `"use client";` directive from the very top of the file.

## Props
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
| `title` | `string` | `"Our Services"` | No | Main heading. |
| `subtitle` | `string` | `"We offer a wide range of web and app services"` | No | Text under the main heading. |
| `services` | `ServiceItem[]` | - | Yes | The service cards: one column on mobile, two on tablets, three on large screens. |
| `services.title` | `string` | - | Yes | Card title. |
| `services.description` | `string` | - | Yes | Card text. |
| `services.icon` | `React.ReactNode` | - | Yes | Icon at the top of the card, e.g. a lucide-react icon. |
| `services.link` | `string` | - | No | Optional URL. Cards without a link render as plain content rather than a link. |
| `services.colSpan` | `1 \| 2` | `1` | No | Set to 2 for a double-width card on medium screens and up. |
| `linkAs` | `React.ElementType` | `"a"` | No | Component used for card links, e.g. next/link's Link. It receives href and className. |
| `className` | `string` | - | No | Extra classes for the root <section>. |
| `...props` | `React.HTMLAttributes<HTMLElement>` | - | No | Any other section attributes (id, style, etc.). |

## Usage
```tsx
"use client"

import {
  Database,
  LayoutTemplate,
  Lightbulb,
  Megaphone,
  MonitorSmartphone,
  ShieldCheck,
} from "lucide-react"
import { ServicesGrid } from "@/components/ui/services-grid"

export default function ServicesGridDemo() {
  const services = [
    {
      title: "Web & Mobile Development",
      description:
        "We build scalable, high-performance web and mobile applications using modern frameworks like Next.js and React Native.",
      icon: <MonitorSmartphone className="h-6 w-6" />,
      link: "#",
    },
    {
      title: "UI/UX Design",
      description:
        "Crafting beautiful, intuitive interfaces that delight users and drive engagement across all platforms.",
      icon: <LayoutTemplate className="h-6 w-6" />,
      link: "#",
    },
    {
      title: "Backend Architecture",
      description:
        "Designing robust, secure, and easily maintainable server-side solutions and APIs for your digital products.",
      icon: <Database className="h-6 w-6" />,
      link: "#",
    },
    {
      title: "Digital Marketing",
      description:
        "Data-driven strategies and SEO optimization to elevate your brand visibility and acquire quality leads.",
      icon: <Megaphone className="h-6 w-6" />,
      link: "#",
    },
    {
      title: "Product Strategy",
      description:
        "Guiding your vision from concept to market-ready product with expert consultation and roadmapping.",
      icon: <Lightbulb className="h-6 w-6" />,
      link: "#",
    },
    {
      title: "Security & Compliance",
      description:
        "Ensuring your applications meet industry standards with comprehensive security audits and data protection.",
      icon: <ShieldCheck className="h-6 w-6" />,
      link: "#",
    },
  ]

  return (
    <div className="relative h-[800px] w-full overflow-hidden overflow-y-auto rounded-xl border border-dashed bg-background">
      <ServicesGrid
        title="Our Services"
        subtitle="End-to-end digital solutions tailored to accelerate your business growth and technical capabilities."
        services={services}
        // linkAs={Link} // Pass your framework's Link component
      />
    </div>
  )
}

```
