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.
| Category | Navigation |
|---|---|
| Type | Web Component (<mv-dock>) |
| Status | stable |
| Keywords | dock, macos, magnification, toolbar, floating-dock, navigation |
When to use
- A portfolio or showcase needs a playful floating app bar with icons that magnify under the pointer
- A small set of icon shortcuts must stay reachable at a screen edge, horizontal or vertical
Avoid when
- The main navigation of a content site needs text labels and many links → use Navbar instead
- An application needs persistent sections with groups and nested items → use Sidebar instead
- Touch is the main input; touch never triggers the magnification or the labels, leaving bare icons
Install
node scripts/add.mjs dock --out ./src/marvelousAI 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
| Name | Type | Default | Description |
|---|---|---|---|
magnification | number | 1.8 | Maximum scale of the icon under the pointer. |
distance | number | 140 | Influence radius in px: beyond it, icons keep their size. |
orientation | horizontal | vertical | horizontal | Dock direction. |
data-side | top | right | Makes icons grow downward (dock at the top) or leftward (vertical dock on the right). |
Content structure
| Name | Description |
|---|---|
<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-separator | Vertical separator (role="separator"). |
CSS classes
| Name | Description |
|---|---|
mv-dock-item | Added to every icon. |
mv-dock-tooltip | Generated label bubble (aria-hidden; the name comes from aria-label). |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-dock-size | 3rem | Base icon size. |
--mv-dock-gap | 0.5rem | Gap between icons. |
--mv-dock-padding | 0.5rem | Bar padding. |
--mv-dock-radius | 1.25rem | Bar radius. |
--mv-dock-bg / --mv-dock-border | Bar background (frosted glass) and border. | |
--mv-dock-dot | var(--mv-fg) | Activity dot color. |
--mv-dock-tooltip-bg / --mv-dock-tooltip-fg | Label 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.