Dock
A floating pill of icon buttons with tooltips, a hover lift and a dot on the active one.
npx shadcn@latest add https://ui.goodtabs.co/r/dock.json
With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/dock
Five apps in the flow, Home active. Hover for the label and the lift.
With a separator
A hairline between the product links and a settings group.
Small
32px icons for a compact bar.
Usage
import { Dock } from "@/components/ui/dock"
<Dock
position="fixed"
items={[
{ label: "Home", href: "/", icon: <HomeIcon />, active: true },
{ label: "Mail", href: "/mail", icon: <MailIcon /> },
{ type: "separator" },
{ label: "Settings", onClick: openSettings, icon: <SettingsIcon /> },
]}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| items | ({ label: string; icon: ReactNode; href?: string; onClick?: () => void; active?: boolean } | { type: "separator" })[] | Icons in order. A separator object draws a hairline between groups. | |
| position | "static" | "fixed" | "static" | static sits in the flow; fixed floats at the bottom centre of the viewport. |
| size | "default" | "sm" | "default" | 40px or 32px icon buttons. |
| label | string | "Dock" | Accessible name of the nav landmark. |
Source
dock/dock.tsx
"use client";
import * as React from "react";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
export type DockItem = {
label: string;
icon: React.ReactNode;
/** Renders a link. */
href?: string;
/** Renders a button. Ignored when href is set. */
onClick?: () => void;
/** Marks the current app with a dot under the icon. */
active?: boolean;
};
export type DockSeparator = { type: "separator" };
type DockProps = Omit<React.ComponentProps<"nav">, "children"> & {
/** Icons in order. Put { type: "separator" } between groups. */
items: (DockItem | DockSeparator)[];
/** static sits in the flow; fixed floats at the bottom centre of the viewport. */
position?: "static" | "fixed";
/** 40px icons by default, 32px for sm. */
size?: "default" | "sm";
/** Accessible name of the nav. */
label?: string;
};
/**
* A row of icon buttons in a floating pill, each with a tooltip.
* The hovered icon lifts 2px; the active one carries a dot underneath.
*/
export function Dock({ items, position = "static", size = "default", label = "Dock", className, ...props }: DockProps) {
return (
<TooltipProvider>
<nav
data-slot="dock"
data-size={size}
aria-label={label}
className={cn(
"inline-flex items-center gap-1 rounded-full border bg-popover shadow-md",
size === "sm" ? "p-1" : "p-1.5",
position === "fixed" && "fixed bottom-4 left-1/2 z-40 -translate-x-1/2",
className,
)}
{...props}
>
{items.map((item, i) =>
"type" in item ? (
<span
key={`separator-${i}`}
role="separator"
aria-orientation="vertical"
className={cn("mx-1 w-px shrink-0 bg-border", size === "sm" ? "h-5" : "h-6")}
/>
) : (
<DockButton key={`${item.label}-${i}`} item={item} size={size} />
),
)}
</nav>
</TooltipProvider>
);
}
function DockButton({ item, size }: { item: DockItem; size: "default" | "sm" }) {
const className = cn(
"relative rounded-full transition-[background-color,color,transform] duration-150 ease-out hover:bg-muted motion-safe:hover:-translate-y-0.5",
size === "sm" ? "size-8 [&_svg:not([class*='size-'])]:size-4" : "size-10 [&_svg:not([class*='size-'])]:size-5",
);
const content = (
<>
{item.icon}
<span className="sr-only">{item.label}</span>
{item.active && (
<span
aria-hidden="true"
data-slot="dock-dot"
className="absolute -bottom-1 left-1/2 size-1 -translate-x-1/2 rounded-full bg-foreground"
/>
)}
</>
);
return (
<Tooltip>
<TooltipTrigger asChild>
{item.href ? (
<Button variant="ghost" size="icon" asChild className={className}>
<a href={item.href} aria-current={item.active ? "page" : undefined}>
{content}
</a>
</Button>
) : (
<Button
type="button"
variant="ghost"
size="icon"
onClick={item.onClick}
aria-current={item.active ? "true" : undefined}
className={className}
>
{content}
</Button>
)}
</TooltipTrigger>
<TooltipContent side="top" sideOffset={8}>
{item.label}
</TooltipContent>
</Tooltip>
);
}
For AI agents
Hand your agent this page, the markdown at /components/dock/prompt.md, or the Copy AI prompt button above.
When to use
- Three to eight top-level destinations that fit as icons
- A floating bar at the bottom of a portfolio, a tool or a mobile layout
- When the labels can live in tooltips because the icons are familiar
When not to use
- When the icons are not obvious: a labelled sidebar or navbar reads faster
- For more than eight items; a dock that scrolls stops being a dock
- Inside a page section as a toolbar, use toolbar instead
Agent notes
- Items with href render anchors, the rest render buttons with onClick; pass handlers from a client component
- position="fixed" pins it at bottom-4 of the viewport at z-40, so leave room under the page content
- The lift is a 2px transform over 150ms and is off under prefers-reduced-motion; the dot marks active without motion
- Tooltips come from shadcn tooltip and open on hover and focus; each icon also has an sr-only label