Dock — <mv-dock>

macOS-style dock: icons grow with their distance to the pointer (spring-smoothed cosine falloff), labels on hover, activity dot, horizontal or vertical, full keyboard navigation.

CategoryNavigation
TypeWeb Component (<mv-dock>)
Statusstable
Keywordsdock, macos, magnification, toolbar, floating-dock, navigation

When to use

Avoid when

Install

node scripts/add.mjs dock --out ./src/marvelous

AI agent with the Marvelous UI MCP server: install_components({ slugs: ["dock"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

Files copied (dependencies included): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/focus.js, core/motion.js, core/observe.js, components/dock/dock.js, components/dock/dock.css.

Usage

Canonical markup — start from it and customize with attributes, data-* and CSS variables:

<div id="dock-demo" class="dk-desk">
  <style>
    #dock-demo { position: relative; width: 100%; height: 26rem; border-radius: var(--mv-radius-xl); overflow: hidden;
      background:
        radial-gradient(60% 80% at 15% 20%, oklch(0.78 0.14 300 / .9), transparent 60%),
        radial-gradient(50% 70% at 85% 30%, oklch(0.75 0.15 220 / .85), transparent 60%),
        radial-gradient(70% 70% at 60% 110%, oklch(0.72 0.17 20 / .8), transparent 60%),
        light-dark(oklch(0.93 0.03 280), oklch(0.25 0.05 280)); }
    #dock-demo::after { content: ""; position: absolute; inset: 0; background: light-dark(transparent, oklch(0 0 0 / .35)); pointer-events: none; }
    #dock-demo .dk-clock { position: absolute; top: 2.25rem; left: 50%; translate: -50% 0; z-index: 1; text-align: center; color: #fff; text-shadow: 0 2px 20px oklch(0 0 0 / .25); }
    #dock-demo .dk-clock b { display: block; font-size: 3.5rem; font-weight: 600; letter-spacing: -.03em; line-height: 1; }
    #dock-demo .dk-clock span { font-size: .9rem; font-weight: 500; opacity: .9; }
    #dock-demo mv-dock { position: absolute; z-index: 1; }
    #dock-demo .dk-bottom { bottom: 1rem; left: 50%; translate: -50% 0; }
    #dock-demo .dk-left { left: 1rem; top: 50%; translate: 0 -50%; }
    #dock-demo .dk-ico { display: grid; place-items: center; color: #fff;
      background: linear-gradient(160deg, oklch(0.78 0.14 var(--h1)), oklch(0.55 0.2 var(--h2)));
      box-shadow: inset 0 1px 0 oklch(1 0 0 / .35), 0 2px 6px oklch(0 0 0 / .18); }
    #dock-demo .dk-ico svg { width: 56%; height: 56%; }
    @media (max-width: 640px) { #dock-demo .dk-left { display: none; } #dock-demo mv-dock { --mv-dock-size: 2.25rem; } }
  </style>
  <div class="dk-clock" aria-hidden="true"><b>09:41</b><span>Tuesday, September 22</span></div>

  <mv-dock class="dk-left" orientation="vertical" aria-label="Navigation" magnification="1.6" distance="110" style="--mv-dock-size: 2.5rem">
    <button type="button" data-label="Home" data-active><span class="dk-ico" style="--h1:265;--h2:285"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M4 11 12 4l8 7v9H4z"/><path d="M10 20v-5h4v5"/></svg></span></button>
    <button type="button" data-label="Search"><span class="dk-ico" style="--h1:200;--h2:225"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="6.5"/><path d="m20 20-4-4"/></svg></span></button>
    <button type="button" data-label="Favorites"><span class="dk-ico" style="--h1:45;--h2:25"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="m12 3.5 2.6 5.3 5.9.9-4.3 4.1 1 5.8L12 16.9l-5.2 2.7 1-5.8-4.3-4.1 5.9-.9z"/></svg></span></button>
    <button type="button" data-label="Profile"><span class="dk-ico" style="--h1:160;--h2:185"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8.5" r="3.8"/><path d="M4.5 20a7.5 7.5 0 0 1 15 0"/></svg></span></button>
  </mv-dock>

  <mv-dock class="dk-bottom" aria-label="Applications">
    <button type="button" data-label="Files" data-active><span class="dk-ico" style="--h1:235;--h2:205"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg></span></button>
    <button type="button" data-label="Browser" data-active><span class="dk-ico" style="--h1:210;--h2:250"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="m15.5 8.5-2 5-5 2 2-5z" fill="currentColor"/></svg></span></button>
    <button type="button" data-label="Messages" data-active><span class="dk-ico" style="--h1:150;--h2:135"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a8 8 0 0 1-11.6 7.1L4 20l1-4.6A8 8 0 1 1 21 12z"/></svg></span></button>
    <button type="button" data-label="Mail"><span class="dk-ico" style="--h1:245;--h2:220"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="2.5"/><path d="m3.5 7 8.5 6 8.5-6"/></svg></span></button>
    <button type="button" data-label="Calendar"><span class="dk-ico" style="--h1:25;--h2:5"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="3.5" y="4.5" width="17" height="16" rx="2.5"/><path d="M3.5 9.5h17M8 3v3M16 3v3"/><path d="M9 14h2v2H9z" fill="currentColor"/></svg></span></button>
    <button type="button" data-label="Photos"><span class="dk-ico" style="--h1:330;--h2:40"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="7.5" r="3.2"/><circle cx="16.5" cy="12" r="3.2"/><circle cx="12" cy="16.5" r="3.2"/><circle cx="7.5" cy="12" r="3.2"/></svg></span></button>
    <button type="button" data-label="Music" data-active><span class="dk-ico" style="--h1:5;--h2:340"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18V5l11-2v13"/><circle cx="6.5" cy="18" r="2.5"/><circle cx="17.5" cy="16" r="2.5"/></svg></span></button>
    <button type="button" data-label="Notes"><span class="dk-ico" style="--h1:85;--h2:65"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M5 4h14v16H5z"/><path d="M8.5 9h7M8.5 12.5h7M8.5 16h4"/></svg></span></button>
    <button type="button" data-label="Settings"><span class="dk-ico" style="--h1:265;--h2:250"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M12 2.5v3M12 18.5v3M4.2 6.2l2.1 2.1M17.7 15.7l2.1 2.1M2.5 12h3M18.5 12h3M4.2 17.8l2.1-2.1M17.7 8.3l2.1-2.1"/></svg></span></button>
    <span class="mv-dock-separator" role="separator"></span>
    <button type="button" data-label="Downloads"><span class="dk-ico" style="--h1:220;--h2:200"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M12 4v11M7 10.5l5 5 5-5M5 20h14"/></svg></span></button>
    <button type="button" data-label="Trash"><span class="dk-ico" style="--h1:260;--h2:250"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7h16M9.5 7V4.5h5V7M6.5 7l1 13h9l1-13"/></svg></span></button>
  </mv-dock>
  <script type="module">
    // Demo: launching an app lights its dot; the side dock behaves like tabs.
    const root = document.getElementById("dock-demo");
    root.querySelector(".dk-bottom").addEventListener("click", (e) => {
      const item = e.target.closest(".mv-dock-item");
      if (item && item.dataset.label !== "Trash") item.setAttribute("data-active", "");
    });
    const side = root.querySelector(".dk-left");
    side.addEventListener("click", (e) => {
      const item = e.target.closest(".mv-dock-item");
      if (!item) return;
      for (const el of side.querySelectorAll(".mv-dock-item")) {
        el.toggleAttribute("data-active", el === item);
        el.setAttribute("aria-pressed", String(el === item));
      }
    });
  </script>
</div>

API

Attributes

NameTypeDefaultDescription
magnificationnumber1.8Maximum scale of the icon under the pointer.
distancenumber140Influence radius in px: beyond it, icons keep their size.
orientationhorizontal | verticalhorizontalDock direction.
data-sidetop | rightMakes icons grow downward (dock at the top) or leftward (vertical dock on the right).

Content structure

NameDescription
<a> / <button> / [data-dock-item]Icons (direct children). data-label = label (becomes aria-label); data-active or aria-current = activity dot; data-no-bounce disables the bounce on click.
.mv-dock-separatorVertical separator (role="separator").

CSS classes

NameDescription
mv-dock-itemAdded to every icon.
mv-dock-tooltipGenerated label bubble (aria-hidden; the name comes from aria-label).

CSS variables

NameDefaultDescription
--mv-dock-size3remBase icon size.
--mv-dock-gap0.5remGap between icons.
--mv-dock-padding0.5remBar padding.
--mv-dock-radius1.25remBar radius.
--mv-dock-bg / --mv-dock-borderBar background (frosted glass) and border.
--mv-dock-dotvar(--mv-fg)Activity dot color.
--mv-dock-tooltip-bg / --mv-dock-tooltip-fgLabel colors.

Accessibility

role=toolbar + aria-orientation, roving tabindex (a single tab stop; arrows/Home/End to navigate). Keyboard focus magnifies the targeted icon and shows its label. Accessible name via data-label → aria-label; aria-current=page for the active item. Reduced motion: no magnification or bounce, labels remain. Transforms only: no reflow during the animation.