Skip to content
goodtabs UI
BrowseBrowse components

Integrations grid

A grid of integration tiles, each with a letter mark, a name and one line.

npx shadcn@latest add https://ui.goodtabs.co/r/integrations-grid.json

With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/integrations-grid

Integrations

Connect your stack

  • PlotlineSync events to your analytics.
  • HarborDeploy previews on every push.
  • QuillPull copy from your content base.
  • LedgerlySend invoices from a form.

Four tiles in a narrow box.

Three columns

Integrations

Works with the tools you already use

Each connection takes one step and can be removed at any time.

  • PlotlineSync events to your analytics.
  • HarborDeploy previews on every push.
  • QuillPull copy from your content base.
  • LedgerlySend invoices from a form.
  • Northwind MailTrigger emails from sign ups.
  • TesselMirror tickets into issues.

Six tiles with a description.

Linked tiles

Each tile is an anchor with hover and focus states.

Usage

import { IntegrationsGrid } from "@/components/integrations-grid"

<IntegrationsGrid title="Connect your stack" items={[{ name: "Plotline", description: "Sync events to your analytics." }]} />

Props

PropTypeDefaultDescription
eyebrowstringSmall uppercase line above the title.
titleReactNodeThe section heading.
descriptionReactNodeOne or two sentences under the title.
items{ name: string; description: string; href?: string }[]The tiles. href turns a tile into a link.
columns2 | 3 | 43Columns from the sm breakpoint up.
size"sm" | "default""default"Spacing scale. sm fits a card.

Source

integrations-grid/integrations-grid.tsx
import * as React from "react";
import { cn } from "@/lib/utils";

export type Integration = {
  /** Tool name. The first letter becomes the tile mark. */
  name: string;
  /** One line on what the connection does. */
  description: string;
  /** Optional link. The tile becomes an anchor. */
  href?: string;
};

type IntegrationsGridProps = Omit<React.ComponentProps<"section">, "title"> & {
  /** Small uppercase line above the title. */
  eyebrow?: string;
  /** The section heading. */
  title?: React.ReactNode;
  /** One or two sentences under the title. */
  description?: React.ReactNode;
  /** The tiles, in the order shown. */
  items: Integration[];
  /** Columns from the sm breakpoint up. */
  columns?: 2 | 3 | 4;
  /** Spacing scale. "sm" fits a card or narrow column. */
  size?: "sm" | "default";
};

const cols = {
  2: "sm:grid-cols-2",
  3: "sm:grid-cols-2 lg:grid-cols-3",
  4: "sm:grid-cols-2 lg:grid-cols-4",
};

const tile = "flex w-full items-start gap-3 rounded-xl border bg-card p-3 text-card-foreground transition-colors";

function Tile({ item }: { item: Integration }) {
  const body = (
    <>
      <span
        aria-hidden="true"
        className="grid size-9 shrink-0 place-items-center rounded-lg bg-muted font-mono text-sm font-medium text-foreground"
      >
        {item.name.charAt(0).toUpperCase()}
      </span>
      <span className="flex min-w-0 flex-col gap-0.5">
        <span className="text-sm font-medium">{item.name}</span>
        <span className="text-sm text-muted-foreground">{item.description}</span>
      </span>
    </>
  );
  if (item.href) {
    return (
      <a
        href={item.href}
        className={cn(
          tile,
          "outline-none hover:bg-muted/50 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50",
        )}
      >
        {body}
      </a>
    );
  }
  return <div className={tile}>{body}</div>;
}

/**
 * A grid of integration tiles: a letter mark, a name and one line each.
 * Server component. The mark is a monogram on purpose, so you never ship a third party's logo by accident.
 */
export function IntegrationsGrid({
  eyebrow,
  title,
  description,
  items,
  columns = 3,
  size = "default",
  className,
  ...props
}: IntegrationsGridProps) {
  const sm = size === "sm";
  return (
    <section className={cn(sm ? "py-8" : "py-16 lg:py-24", className)} {...props}>
      <div className="mx-auto w-full max-w-6xl px-4 md:px-6">
        {(eyebrow || title || description) && (
          <header className={cn("flex max-w-2xl flex-col gap-2", sm ? "mb-6" : "mb-10")}>
            {eyebrow && (
              <p className="m-0 font-mono text-xs tracking-wide text-muted-foreground uppercase">{eyebrow}</p>
            )}
            {title && (
              <h2 className={cn("m-0 font-medium tracking-tight text-balance", sm ? "text-xl" : "text-3xl")}>
                {title}
              </h2>
            )}
            {description && <p className="m-0 text-sm text-muted-foreground sm:text-base">{description}</p>}
          </header>
        )}
        <ul className={cn("m-0 grid list-none grid-cols-1 gap-3 p-0", cols[columns])}>
          {items.map((item) => (
            <li key={item.name} className="flex">
              <Tile item={item} />
            </li>
          ))}
        </ul>
      </div>
    </section>
  );
}

For AI agents

Hand your agent this page, the markdown at /components/integrations-grid/prompt.md, or the Copy AI prompt button above.

When to use

  • A page that lists the tools your product connects to
  • When names and one line each are the whole message

When not to use

  • For a searchable directory of many apps
  • When you need real brand logos; this uses a monogram on purpose

Agent notes

  • Server component with no state
  • The mark is the first letter of the name, so no third party logo ships by accident
  • All tool names in the demos are made up