Skip to content
goodtabs UI
BrowseBrowse components

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

PropTypeDefaultDescription
namestringThe service's name. Also names the switch: Connect {name}.
descriptionstringOne line on what connecting does.
iconReactNodeThe logo in currentColor. Initials show when it is missing.
connectedbooleanControlled state.
defaultConnectedbooleanfalseStarting state when uncontrolled.
onConnectedChange(connected: boolean) => voidCalled with the next state when the switch flips.
disabledbooleanfalseLocks 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