# Launch Dock Logos

> A logo cloud that tells you why each logo is there. The customers' marks sit quiet in a dock; pick one, or let it walk through them, and its mark takes its color while a small card launches out of it with the one number its story is about and a link to the rest.

A shadcn/ui block from the [@keyframeui](https://keyframeui.com/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/keyframeui/launch-dock-logos).

- Page, with a live preview: https://shadcn.exchange/r/keyframeui/launch-dock-logos
- Screenshot: https://shadcn.exchange/shot/keyframeui/launch-dock-logos/light.png
- Registry item: https://keyframeui.com/r/launch-dock-logos.json
- Categories: blocks, logo-clouds
- Listed: 2026-11-02, source last changed 2026-11-02

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/keyframeui/launch-dock-logos.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- Registry items: `https://keyframeui.com/r/launch-dock.json`

## Notes from the registry

Live preview and the components it's built with: https://keyframeui.com/blocks/logo-clouds/launch-dock-logos

## Files

### `registry/blocks/launch-dock-logos.tsx`

```tsx
"use client";

import * as React from "react";

import { LaunchDock, type LaunchDockItem } from "@/components/ui/launch-dock";

/* ── Content ─────────────────────────────────────────────────
 * Everything you'd change to make the section yours: the line
 * over the logos, and each customer's mark, color and the one
 * number their story is about. The marks are small SVGs; swap
 * in your customers' logos.
 * ─────────────────────────────────────────────────────────── */

const LABEL = "Teams that run on Shoal";
const HINT = "Pick a logo to see what changed.";
const STORY_LINK = "Read the story";

type Customer = {
  id: string;
  name: string;
  /** The mark's color once its story is open. */
  color: string;
  /** The mark: SVG shapes on a 24-unit grid, drawn in the current color. */
  mark: React.ReactNode;
  metric: string;
  metricLabel: string;
  story: string;
  href: string;
};

const CUSTOMERS: Customer[] = [
  {
    id: "northwind",
    name: "Northwind Air",
    color: "#2563eb",
    mark: <path d="M3 15.5 21 6l-6.5 13-2.2-5.3Z" />,
    metric: "6 sec",
    metricLabel: "from a gate tablet to the manifest",
    story: "2,400 crew tablets check passengers in at the gate, online or not, and every one lands in the manifest.",
    href: "#northwind",
  },
  {
    id: "harbor",
    name: "Harbor",
    color: "#0d9488",
    mark: (
      <>
        <circle cx="12" cy="6.5" r="2.5" />
        <path d="M11 9h2v8.2a5.5 5.5 0 0 0 4.6-3.7l-1.7.4L18.5 11l1.3 4.6-1.6-.6A7.5 7.5 0 0 1 12 20a7.5 7.5 0 0 1-6.2-5l-1.6.6L5.5 11l2.6 2.9-1.7-.4A5.5 5.5 0 0 0 11 17.2Z" />
      </>
    ),
    metric: "11 hrs",
    metricLabel: "back per inspector, every week",
    story: "Field inspections write to the phone first and sync when the truck finds signal. No more evening data entry.",
    href: "#harbor",
  },
  {
    id: "kvarter",
    name: "Kvarter",
    color: "#ea580c",
    mark: (
      <>
        <rect x="4" y="4" width="7" height="7" rx="1" />
        <rect x="13" y="4" width="7" height="7" rx="3.5" />
        <rect x="4" y="13" width="7" height="7" rx="3.5" />
        <rect x="13" y="13" width="7" height="7" rx="1" />
      </>
    ),
    metric: "−92%",
    metricLabel: "merge conflicts on shared plans",
    story: "Thirty architects edit one floor plan at a time of day that used to mean overwriting each other.",
    href: "#kvarter",
  },
  {
    id: "okto",
    name: "Okto",
    color: "#7c3aed",
    mark: <path d="M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Zm0 4.5a4.5 4.5 0 1 1 0 9 4.5 4.5 0 0 1 0-9Z" fillRule="evenodd" />,
    metric: "40 ms",
    metricLabel: "median note sync, phone to laptop",
    story: "Meeting notes appear on every attendee's device before the next agenda item starts.",
    href: "#okto",
  },
  {
    id: "ferngrove",
    name: "Ferngrove Schools",
    color: "#16a34a",
    mark: <path d="M12 2 4.5 14h4L6 19h12l-2.5-5h4Z" />,
    metric: "180",
    metricLabel: "schools on one timetable",
    story: "A room change made in the office reaches every teacher's tablet before the bell, even in the basement labs.",
    href: "#ferngrove",
  },
  {
    id: "tinta",
    name: "Tinta Print",
    color: "#db2777",
    mark: (
      <>
        <path d="M12 3c3 4 6 7 6 10.5a6 6 0 0 1-12 0C6 10 9 7 12 3Z" />
      </>
    ),
    metric: "3×",
    metricLabel: "orders through the shop floor",
    story: "Press operators mark jobs done on a tablet by the machine, and the front desk sees it in the same second.",
    href: "#tinta",
  },
];

/* ── Section ─────────────────────────────────────────────── */

export function LaunchDockLogos() {
  const [open, setOpen] = React.useState(CUSTOMERS[0].id);

  // The open customer's mark takes its color; the rest stay quiet, like a logo wall.
  const items: LaunchDockItem[] = CUSTOMERS.map((customer) => ({
    id: customer.id,
    label: customer.name,
    icon: <Mark customer={customer} lit={customer.id === open} />,
    content: <Story customer={customer} />,
  }));

  return (
    <section className="bg-white text-zinc-950 dark:bg-zinc-950 dark:text-white">
      <div className="mx-auto max-w-6xl px-6 py-20 sm:py-24">
        <div className="text-center">
          <h2 className="text-sm font-semibold tracking-tight">{LABEL}</h2>
          <p className="mt-1 text-sm text-zinc-500 dark:text-zinc-400">{HINT}</p>
        </div>
        <LaunchDock
          items={items}
          value={open}
          onValueChange={(id) => setOpen(id)}
          frame="card"
          fade={false}
          autoplay={4500}
          windowSize={[560, 236]}
          dockLabel="Customers"
          className="mx-auto mt-8 max-w-[560px]"
          trayClassName="bg-zinc-50/80 shadow-none ring-zinc-950/[0.06] dark:bg-white/[0.04] dark:shadow-none"
          windowClassName="bg-zinc-50 ring-zinc-950/[0.06] dark:bg-zinc-900"
        />
      </div>
    </section>
  );
}

function Mark({ customer, lit }: { customer: Customer; lit: boolean }) {
  return (
    <span className="grid size-full place-items-center bg-white dark:bg-zinc-900">
      <svg
        viewBox="0 0 24 24"
        aria-hidden
        style={{ color: lit ? customer.color : undefined }}
        className="size-[56%] fill-current text-zinc-400 transition-colors duration-300 motion-reduce:transition-none dark:text-zinc-500"
      >
        {customer.mark}
      </svg>
    </span>
  );
}

function Story({ customer }: { customer: Customer }) {
  return (
    <div className="flex h-full flex-col justify-between px-9 py-8">
      <div className="flex items-center justify-between">
        <p className="text-[13px] font-semibold tracking-[0.08em] text-zinc-500 uppercase dark:text-zinc-400">{customer.name}</p>
        <a
          href={customer.href}
          className="inline-flex items-center gap-1 rounded-sm text-[14px] font-semibold text-zinc-950 underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-zinc-950 dark:text-white dark:focus-visible:outline-white"
        >
          {STORY_LINK}
          <svg viewBox="0 0 16 16" className="size-4" fill="none" stroke="currentColor" strokeWidth={1.6} aria-hidden>
            <path d="M3.5 8h9m-3.5-3.5L12.5 8 9 11.5" strokeLinecap="round" strokeLinejoin="round" />
          </svg>
        </a>
      </div>
      <p className="flex items-baseline gap-3">
        <span style={{ color: customer.color }} className="text-[46px] leading-none font-semibold tracking-[-0.04em] tabular-nums">
          {customer.metric}
        </span>
        <span className="text-[16px] text-zinc-600 dark:text-zinc-400">{customer.metricLabel}</span>
      </p>
      <p className="text-[16.5px] leading-relaxed text-pretty text-zinc-800 dark:text-zinc-200">{customer.story}</p>
    </div>
  );
}
```
