UIPackage
Menu

Dock

dock ui
Edit on GitHub

macOS-style dock menu with magnification on hover. Items expand as the cursor approaches using a cosine bell curve. Supports an items array (icon + label + handler), magnification scale, base size, tooltips on hover, click handlers, and an active state indicator.

Also available for Vue ->

Installation

$ npx shadcn@latest add https://uipkge.dev/r/react/dock.json
Named registry: npx shadcn@latest add @uipkge-react/dock Installs to: components/ui/dock/

Examples

Props

Name Type / Values Default Required
items

Dock items.

DockItem[] required
baseSize

Base icon size in pixels. Default 48.

number optional
magnification

Peak magnification scale as the cursor hovers directly over an item. Default 1.6.

number optional
distance

Pixel radius within which items magnify. Default 120.

number optional
orientation

Orientation. Only 'horizontal' (bottom dock) is supported.

'horizontal' optional
showTooltips

Show the tooltip label on hover. Default true.

boolean optional

Schema

Type aliases from this item's source — use them to shape the data you pass in.

DockItem
interface DockItem {
  /** Unique id for the item. */
  id: string
  /** Lucide icon component to render. */
  icon: LucideIcon
  /** Label shown in the tooltip on hover. */
  label: string
  /** Click handler. */
  handler?: () => void
  /** Whether this item is the active one. */
  active?: boolean
}

npm dependencies

Files installed (2)

  • components/ui/dock/Dock.tsx 5.1 kB
    import * as React from 'react'
    import type { LucideIcon } from 'lucide-react'
    import { cn } from '@/lib/utils'
    
    export interface DockItem {
      /** Unique id for the item. */
      id: string
      /** Lucide icon component to render. */
      icon: LucideIcon
      /** Label shown in the tooltip on hover. */
      label: string
      /** Click handler. */
      handler?: () => void
      /** Whether this item is the active one. */
      active?: boolean
    }
    
    export interface DockProps extends React.HTMLAttributes<HTMLDivElement> {
      /** Dock items. */
      items: DockItem[]
      /** Base icon size in pixels. Default 48. */
      baseSize?: number
      /** Peak magnification scale as the cursor hovers directly over an item. Default 1.6. */
      magnification?: number
      /** Pixel radius within which items magnify. Default 120. */
      distance?: number
      /** Orientation. Only 'horizontal' (bottom dock) is supported. */
      orientation?: 'horizontal'
      /** Show the tooltip label on hover. Default true. */
      showTooltips?: boolean
    }
    
    const Dock = React.forwardRef<HTMLDivElement, DockProps>(
      (
        {
          items,
          baseSize = 48,
          magnification = 1.6,
          distance = 120,
          orientation = 'horizontal',
          showTooltips = true,
          className,
          ...props
        },
        ref,
      ) => {
        const mouseXRef = React.useRef<number | null>(null)
        const [mouseX, setMouseX] = React.useState<number | null>(null)
        const [hoveredId, setHoveredId] = React.useState<string | null>(null)
        const itemRefs = React.useRef<(HTMLDivElement | null)[]>([])
    
        function onMove(e: React.MouseEvent<HTMLDivElement>) {
          mouseXRef.current = e.clientX
          setMouseX(e.clientX)
        }
    
        function onLeave() {
          mouseXRef.current = null
          setMouseX(null)
          setHoveredId(null)
        }
    
        function sizeFor(index: number): number {
          if (mouseXRef.current === null) return baseSize
          const el = itemRefs.current[index]
          if (!el) return baseSize
          const rect = el.getBoundingClientRect()
          const center = rect.left + rect.width / 2
          const dist = Math.abs(mouseXRef.current - center)
          if (dist > distance) return baseSize
          // Cosine bell curve so magnification falls off smoothly.
          const t = 1 - dist / distance
          const scale = 1 + (magnification - 1) * t
          return baseSize * scale
        }
    
        const sizes = items.map((_, i) => sizeFor(i))
    
        return (
          <div
            ref={ref}
            data-uipkge=""
            data-slot="dock"
            data-orientation={orientation}
            className={cn(
              'border-border/60 bg-background/60 flex items-end justify-center gap-3 rounded-2xl border px-3 py-2 backdrop-blur-md',
              className,
            )}
            onMouseMove={onMove}
            onMouseLeave={onLeave}
            {...props}
          >
            {items.map((item, index) => {
              const Icon = item.icon
              const size = sizes[index] ?? baseSize
              return (
                <div
                  key={item.id}
                  ref={(el) => {
                    itemRefs.current[index] = el
                  }}
                  data-slot="dock-item"
                  data-active={item.active ? '' : undefined}
                  role="button"
                  tabIndex={0}
                  aria-label={item.label}
                  aria-current={item.active ? 'true' : undefined}
                  className="group focus-visible:ring-ring/50 relative flex shrink-0 cursor-pointer items-end justify-center rounded-xl outline-none focus-visible:ring-[3px]"
                  onMouseEnter={() => setHoveredId(item.id)}
                  onMouseLeave={() => setHoveredId(null)}
                  onClick={() => item.handler?.()}
                  onKeyDown={(e) => {
                    if (e.key === 'Enter') {
                      item.handler?.()
                    } else if (e.key === ' ') {
                      e.preventDefault()
                      item.handler?.()
                    }
                  }}
                >
                  {/* Tooltip */}
                  {showTooltips && hoveredId === item.id ? (
                    <span className="border-border bg-popover text-popover-foreground pointer-events-none absolute -top-9 left-1/2 -translate-x-1/2 rounded-md border px-2 py-1 text-xs whitespace-nowrap shadow-md">
                      {item.label}
                    </span>
                  ) : null}
    
                  {/* Icon tile */}
                  <span
                    className={cn(
                      'flex items-center justify-center rounded-xl border transition-[width,height] duration-100 ease-out will-change-[width,height]',
                      item.active
                        ? 'border-primary/40 bg-primary/10 text-primary'
                        : 'border-border/50 bg-muted/40 text-foreground hover:bg-muted',
                    )}
                    style={{ width: `${size}px`, height: `${size}px` }}
                  >
                    <Icon style={{ width: `${size * 0.5}px`, height: `${size * 0.5}px` }} />
                  </span>
    
                  {/* Active indicator dot */}
                  {item.active ? (
                    <span className="bg-primary absolute -bottom-1.5 size-1 rounded-full" aria-hidden="true" />
                  ) : null}
                </div>
              )
            })}
          </div>
        )
      },
    )
    Dock.displayName = 'Dock'
    
    export { Dock }
  • components/ui/dock/index.ts 0.1 kB
    export { Dock, type DockProps, type DockItem } from './Dock'

Raw manifest: https://uipkge.dev/r/react/dock.json