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
| Prop | Type | Default | Description |
|---|---|---|---|
| eyebrow | string | Small uppercase line above the title. | |
| title | ReactNode | The section heading. | |
| description | ReactNode | One or two sentences under the title. | |
| items | { name: string; description: string; href?: string }[] | The tiles. href turns a tile into a link. | |
| columns | 2 | 3 | 4 | 3 | Columns 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