Dock Magnify

Icons in a dock swell as the pointer nears them and shrink as it moves away, pushing neighbours aside like a desktop dock.

  • JS
  • Layout
  • Pointer
  • Added

AI prompt

Create a magnifying app dock in React. Lay out icon buttons in a flex row aligned to the bottom. On pointermove over the dock, for each icon compute the horizontal distance from the pointer to the icon centre and a scale of 1 + 0.7 × max(0, 1 − distance / 110), and write it into a CSS custom property --s on the icon (no React state). Size the icon with width and height calc(40px × var(--s)) rather than transform, so growing icons push their neighbours apart, and transition width and height over 120ms ease-out. On pointerleave, reset every --s to 1. Each button has an aria-label and shows its name as a small label above when hovered or focused. Under prefers-reduced-motion, keep icons at a fixed size. The apps are Mail, Notes, Music, Photos, Maps, and Files.

Source

Demo.tsx
'use client'

import { useRef } from 'react'
import s from './demo.module.css'

const REACH = 110

export default function DockMagnify({ labels }: { labels: Record<string, string> }) {
  const dock = useRef<HTMLDivElement>(null)
  const items = labels.items.split('|')

  const onMove = (e: React.PointerEvent) => {
    if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return
    for (const el of Array.from(dock.current?.children ?? []) as HTMLElement[]) {
      const r = el.getBoundingClientRect()
      const d = Math.abs(e.clientX - (r.left + r.width / 2))
      el.style.setProperty('--s', String(1 + 0.7 * Math.max(0, 1 - d / REACH)))
    }
  }
  const onLeave = () => {
    for (const el of Array.from(dock.current?.children ?? []) as HTMLElement[]) el.style.setProperty('--s', '1')
  }

  return (
    <div ref={dock} className={s.dock} role="toolbar" aria-label={labels.dock} onPointerMove={onMove} onPointerLeave={onLeave}>
      {items.map((name, i) => (
        <button key={name} type="button" className={s.icon} aria-label={name} style={{ '--h': i * 55 } as React.CSSProperties}>
          <span className={s.tip} aria-hidden="true">
            {name}
          </span>
          {Array.from(name)[0]}
        </button>
      ))}
    </div>
  )
}
demo.module.css
.dock {
  display: flex;
  align-items: flex-end;
  gap: 8px;
  height: 96px;
  padding: 8px 10px;
  border: 1px solid var(--stage-rule);
  border-radius: 18px;
  background: var(--stage-surface);
  align-self: end;
  margin-bottom: 24px;
}

.icon {
  --s: 1;
  position: relative;
  display: grid;
  place-items: center;
  width: calc(40px * var(--s));
  height: calc(40px * var(--s));
  padding: 0;
  border: 0;
  border-radius: calc(10px * var(--s));
  background: oklch(62% 0.11 calc(40 + var(--h)));
  color: oklch(20% 0.02 60);
  font: 700 calc(var(--text-base) * var(--s)) / 1 var(--font-display);
  cursor: pointer;
  transition: width 120ms var(--ease-out), height 120ms var(--ease-out), border-radius 120ms var(--ease-out), font-size 120ms var(--ease-out);
}

.icon:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 3px; }

.tip {
  position: absolute;
  bottom: calc(100% + 8px);
  left: 50%;
  translate: -50% 0;
  padding: 4px 7px;
  border-radius: 6px;
  background: var(--stage-ink);
  color: var(--stage-bg);
  font: 500 var(--text-xs) / 1 var(--font-body);
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity 120ms;
}

.icon:hover .tip,
.icon:focus-visible .tip { opacity: 1; }

@media (max-width: 380px) {
  .dock { gap: 5px; }
  .icon { width: calc(34px * var(--s)); height: calc(34px * var(--s)); }
}

@media (prefers-reduced-motion: reduce) {
  .icon { transition: none; }
}

Suggest an idea

Seen a motion or a theme you would like to find here? Describe it, and it opens as a prefilled GitHub issue for you to post.

What are you suggesting?

Needs a GitHub account. Suggestions are public.