A design studio's clients as marks in the dock, each launching the site the studio built for them, address bar and all.
registry/demos/launch-dock-studio-demo.tsx
"use client";
import * as React from "react";
import { LaunchDock, type LaunchDockItem } from "@/components/ui/launch-dock";
const photo = (id: string) => `https://images.unsplash.com/photo-${id}?auto=format&fit=crop&w=1400&h=880&q=80`;
type Project = {
id: string;
client: string;
work: string;
domain: string;
mark: React.ReactNode;
};
function Mark({ bg, children }: { bg: string; children: React.ReactNode }) {
return <span className={`grid size-full place-items-center ${bg}`}>{children}</span>;
}
function Url({ children }: { children: React.ReactNode }) {
return (
<span className="inline-flex min-w-[46%] items-center justify-center gap-1.5 rounded-md bg-zinc-950/[0.05] px-3 py-0.5 dark:bg-white/[0.07]">
<svg viewBox="0 0 12 12" aria-hidden className="size-2.5 opacity-60">
<path d="M3.5 5.5V4a2.5 2.5 0 0 1 5 0v1.5M2.5 5.5h7v5h-7z" fill="none" stroke="currentColor" strokeWidth="1.2" />
</svg>
{children}
</span>
);
}
// A client's site as the studio shipped it: one photo, their type, their nav.
function Site({
src,
alt,
tone,
nav,
brand,
children,
shade = "bg-gradient-to-b from-black/55 via-black/10 to-transparent",
}: {
src: string;
alt: string;
tone: "light" | "dark";
nav: string[];
brand: React.ReactNode;
children: React.ReactNode;
shade?: string;
}) {
return (
<div className={`relative h-full overflow-hidden ${tone === "light" ? "text-white" : "text-zinc-950"}`}>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={src} alt={alt} className="absolute inset-0 size-full object-cover" />
<div className={`absolute inset-0 ${shade}`} />
<header className="relative flex items-center justify-between px-10 py-7">
{brand}
<nav className="flex gap-7 text-[13px] font-medium opacity-90">
{nav.map((link) => (
<span key={link}>{link}</span>
))}
</nav>
</header>
<div className="relative px-10 pt-14">{children}</div>
</div>
);
}
const projects: Project[] = [
{
id: "halden",
client: "Halden House",
work: "Website and identity, 2026",
domain: "halden.house",
mark: (
<Mark bg="bg-stone-200">
<span className="font-serif text-[1.65em] text-stone-800 italic">H</span>
</Mark>
),
},
{
id: "ember",
client: "Ember",
work: "Launch campaign, 2026",
domain: "ember.run",
mark: (
<Mark bg="bg-red-600">
<span className="text-[1.5em] font-black text-white italic">E</span>
</Mark>
),
},
{
id: "tempo",
client: "Tempo",
work: "Product launch site, 2025",
domain: "tempo.watch",
mark: (
<Mark bg="bg-slate-900">
<span className="text-[1.35em] font-light tracking-widest text-white">T</span>
</Mark>
),
},
{
id: "sable",
client: "Sable",
work: "Shop and art direction, 2025",
domain: "sable.store",
mark: (
<Mark bg="bg-zinc-950">
<span className="font-serif text-[1.55em] text-amber-100">S</span>
</Mark>
),
},
{
id: "nordlys",
client: "Nordlys",
work: "Booking site, 2024",
domain: "nordlys.no",
mark: (
<Mark bg="bg-teal-700">
<span className="text-[1.4em] font-semibold text-teal-50">N</span>
</Mark>
),
},
{
id: "carry",
client: "Carry Co.",
work: "Packaging and shop, 2024",
domain: "carry.co",
mark: (
<Mark bg="bg-amber-200">
<span className="font-mono text-[1.35em] font-bold text-amber-900">C</span>
</Mark>
),
},
];
const sites: Record<string, React.ReactNode> = {
halden: (
<Site
src={photo("1600585154340-be6161a56a0c")}
alt="A timber house at dusk with its windows lit"
tone="light"
nav={["Projects", "Practice", "Journal", "Contact"]}
brand={<span className="font-serif text-[22px] italic">Halden House</span>}
shade="bg-gradient-to-br from-black/75 via-black/30 to-black/0"
>
<h1 className="max-w-[560px] font-serif text-[54px] leading-[1.04] tracking-[-0.01em]">
Houses that hold the evening light.
</h1>
<p className="mt-5 max-w-[400px] text-[15px] leading-relaxed text-white/80">
A small practice on the west coast, building eight homes a year in timber, stone and glass.
</p>
</Site>
),
ember: (
<Site
src={photo("1542291026-7eec264c27ff")}
alt="A red running shoe on a red background"
tone="light"
nav={["Run", "Train", "Stories", "Bag (0)"]}
brand={<span className="text-[22px] font-black tracking-tight italic">EMBER</span>}
shade="bg-gradient-to-b from-black/45 via-transparent to-transparent"
>
<h1 className="text-[92px] leading-[0.86] font-black tracking-[-0.04em] uppercase italic">
Run
<br />
warm.
</h1>
<span className="mt-7 inline-block rounded-full bg-white px-6 py-3 text-[14px] font-bold text-red-600">
Shop the Ember Runner · $140
</span>
</Site>
),
tempo: (
<Site
src={photo("1523275335684-37898b6baf30")}
alt="A white watch on a pale background"
tone="dark"
nav={["Collection", "Craft", "Stores"]}
brand={<span className="text-[18px] font-light tracking-[0.5em]">TEMPO</span>}
shade="bg-gradient-to-b from-white/70 via-white/20 to-transparent"
>
<h1 className="max-w-[420px] text-[46px] leading-[1.1] font-light tracking-[-0.02em]">Time, kept quietly.</h1>
<p className="mt-4 max-w-[340px] text-[15px] leading-relaxed text-zinc-700">
The Model 01 has no logo on its face and no second hand. Just the hour, and enough of the minute.
</p>
</Site>
),
sable: (
<Site
src={photo("1592078615290-033ee584e267")}
alt="A black shell chair against a light wall"
tone="dark"
nav={["Chairs", "Tables", "Lighting", "Cart"]}
brand={<span className="font-serif text-[24px]">Sable</span>}
shade="bg-gradient-to-b from-white/80 via-white/10 to-transparent"
>
<p className="text-[12px] font-semibold tracking-[0.2em] uppercase text-zinc-600">New · The Shell Chair</p>
<h1 className="mt-3 max-w-[460px] font-serif text-[58px] leading-[1.02] tracking-[-0.02em]">Sit with it a while.</h1>
</Site>
),
nordlys: (
<Site
src={photo("1600607687939-ce8a6c25118c")}
alt="A bright open living room with large windows"
tone="light"
nav={["Homes", "Regions", "About"]}
brand={<span className="text-[20px] font-semibold tracking-tight">nordlys</span>}
shade="bg-gradient-to-b from-black/50 via-black/15 to-transparent"
>
<h1 className="max-w-[520px] text-[48px] leading-[1.06] font-semibold tracking-[-0.03em]">
Stay somewhere that feels like yours.
</h1>
<div className="mt-7 flex w-[560px] items-center gap-1 rounded-2xl bg-white p-1.5 text-[13px] text-zinc-500 shadow-xl">
<span className="flex-1 rounded-xl px-4 py-2.5">Lofoten, Norway</span>
<span className="h-6 w-px bg-zinc-200" />
<span className="flex-1 rounded-xl px-4 py-2.5">Feb 14 – 18</span>
<span className="h-6 w-px bg-zinc-200" />
<span className="flex-1 rounded-xl px-4 py-2.5">2 guests</span>
<span className="rounded-xl bg-teal-700 px-5 py-2.5 font-semibold text-white">Search</span>
</div>
</Site>
),
carry: (
<Site
src={photo("1544816155-12df9643f363")}
alt="A canvas tote bag hanging on a wall"
tone="dark"
nav={["Totes", "Duffels", "Repairs", "Bag"]}
brand={<span className="font-mono text-[18px] font-bold">CARRY CO.</span>}
shade="bg-gradient-to-b from-amber-50/85 via-amber-50/20 to-transparent"
>
<h1 className="max-w-[460px] font-mono text-[44px] leading-[1.08] font-bold tracking-[-0.03em]">
Made to be overfilled.
</h1>
<p className="mt-4 max-w-[360px] text-[15px] leading-relaxed text-zinc-700">
18oz canvas, double-stitched handles, and free repairs for as long as you own one.
</p>
</Site>
),
};
const items: (LaunchDockItem & { work: string })[] = projects.map((project) => ({
id: project.id,
label: project.client,
title: <Url>{project.domain}</Url>,
icon: project.mark,
content: sites[project.id],
work: project.work,
}));
export function LaunchDockStudioDemo() {
const [open, setOpen] = React.useState(items[0].id);
const project = items.find((item) => item.id === open) ?? items[0];
return (
<div className="flex w-full max-w-3xl flex-col items-center gap-5">
<LaunchDock items={items} value={open} onValueChange={setOpen} autoplay dockLabel="Clients" />
<p aria-live="polite" className="text-sm text-zinc-500 dark:text-zinc-400">
<span className="font-medium text-zinc-900 dark:text-zinc-100">{project.label}</span> · {project.work}
</p>
</div>
);
}
Live preview, examples and props: https://keyframeui.com/components/launch-dock#launch-dock-studio-demo