Integration card
A service with its logo, a one-line description and a switch that connects it.
npx shadcn@latest add https://ui.goodtabs.co/r/integration-card.json
With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/integration-card
GitHubConnectedLink pull requests and star counts to listings.
One service, connected. The status line follows the switch.
A grid of four
GitHubConnectedLink pull requests and star counts to listings.
LinearNot connectedOpen an issue from any submission.
SlackConnectedPost new listings to a channel as they land.
VercelNot connectedDeploy previews for every content change.
GitHub, Linear, Slack and Vercel with inline SVG marks in currentColor.
Disabled
Google AnalyticsNot connectedAvailable on the Pro plan.
No icon, so initials fill the tile. The switch is locked.
Usage
import { IntegrationCard } from "@/components/ui/integration-card"
<IntegrationCard
name="GitHub"
description="Link pull requests to listings."
icon={<GitHubIcon />}
connected={connected}
onConnectedChange={setConnected}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| name | string | The service's name. Also names the switch: Connect {name}. | |
| description | string | One line on what connecting does. | |
| icon | ReactNode | The logo in currentColor. Initials show when it is missing. | |
| connected | boolean | Controlled state. | |
| defaultConnected | boolean | false | Starting state when uncontrolled. |
| onConnectedChange | (connected: boolean) => void | Called with the next state when the switch flips. | |
| disabled | boolean | false | Locks the switch and fades the card. |
Source
integration-card/integration-card.tsx
"use client";
import * as React from "react";
import { Switch } from "@/components/ui/switch";
import { cn } from "@/lib/utils";
type IntegrationCardProps = Omit<React.ComponentProps<"div">, "children"> & {
/** The service's name. */
name: string;
/** One line on what connecting does. */
description?: string;
/** The logo, as an icon or inline SVG in currentColor. Initials show when it is missing. */
icon?: React.ReactNode;
/** Controlled state. */
connected?: boolean;
/** Starting state when uncontrolled. */
defaultConnected?: boolean;
/** Called with the next state when the switch flips. */
onConnectedChange?: (connected: boolean) => void;
/** Locks the switch and fades the card. */
disabled?: boolean;
};
function initials(name: string) {
const parts = name.trim().split(/\s+/);
return ((parts[0]?.[0] ?? "") + (parts[1]?.[0] ?? "")).toUpperCase();
}
/**
* A service with its logo, a one-line description and a switch that connects it.
* The status line under the name follows the switch.
*/
export function IntegrationCard({
name,
description,
icon,
connected,
defaultConnected = false,
onConnectedChange,
disabled = false,
className,
...props
}: IntegrationCardProps) {
const [internal, setInternal] = React.useState(defaultConnected);
const isConnected = connected ?? internal;
const id = React.useId();
const change = (next: boolean) => {
if (connected === undefined) setInternal(next);
onConnectedChange?.(next);
};
return (
<div
data-slot="integration-card"
data-connected={isConnected || undefined}
className={cn("flex w-full items-start gap-3 rounded-lg border bg-card p-4 text-card-foreground", className)}
{...props}
>
<span
aria-hidden="true"
className={cn(
"inline-grid size-10 shrink-0 place-items-center rounded-md bg-muted text-sm font-medium [&_svg]:size-5",
disabled ? "text-muted-foreground" : "text-foreground",
)}
>
{icon ?? initials(name)}
</span>
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
<span
id={`${id}-name`}
className={cn("truncate text-sm font-medium", disabled ? "text-muted-foreground" : "text-foreground")}
>
{name}
</span>
<span id={`${id}-status`} aria-live="polite" className="font-mono text-xs text-muted-foreground">
{isConnected ? "Connected" : "Not connected"}
</span>
{description && (
<span id={`${id}-description`} className="mt-1 text-sm text-muted-foreground">
{description}
</span>
)}
</div>
<Switch
aria-label={`Connect ${name}`}
aria-describedby={description ? `${id}-description` : undefined}
checked={isConnected}
onCheckedChange={change}
disabled={disabled}
className="mt-0.5 shrink-0"
/>
</div>
);
}
For AI agents
Hand your agent this page, the markdown at /components/integration-card/prompt.md, or the Copy AI prompt button above.
When to use
- Integrations and connected apps pages in settings
- Onboarding steps that ask people to link a few services
- Any list of on and off services with a logo each
When not to use
- Settings that are not a connection, use a switch field
- Connections that need a flow (OAuth, a form): open it from onConnectedChange and keep the card controlled
- Showing a status without letting people change it
Agent notes
- Client component; a demo or page that passes onConnectedChange must be a client component too
- Uncontrolled by default with defaultConnected; pass connected to control it, and flip it back in onConnectedChange if the connection fails
- The icon slot is size-10 bg-muted and sizes any svg to 20px; pass marks in currentColor so they stay monochrome
- Put several in a CSS grid, the card has no fixed width