Skip to content
ShineUI
Install

Dock

The suite switcher.

<sui-dock>Menus and navigationStable

About

App switcher on navy chrome in three orientations: a horizontal bar, a vertical bar and a radial fan around a center launcher. The control that opens and closes it never moves, and the radial fan spaces its own tiles so they never overlap. Labels appear as a capsule on hover and focus.

Usage

<sui-dock aria-label="Applications, Basic" orientation="horizontal" active="Home"></sui-dock> <script type="module"> import { defineCustomElements } from "@shineui/core/loader"; import "@shineui/core/shineui.css"; defineCustomElements(); const el = document.querySelector("sui-dock"); el.items = [ { label: "Home", icon: "fa-solid fa-house" }, { label: "Mail", icon: "fa-regular fa-envelope", badge: 5 }, { label: "Expenses", icon: "fa-regular fa-credit-card", badge: 2 }, { label: "Files", icon: "fa-regular fa-folder-open" }, ]; /* SuiDockItem[] */ el.addEventListener("suiClose", (e) => console.log(e.detail)); </script>
import { SuiDock } from "@shineui/react"; import "@shineui/core/shineui.css"; export function Example() { const items = [ { label: "Home", icon: "fa-solid fa-house" }, { label: "Mail", icon: "fa-regular fa-envelope", badge: 5 }, { label: "Expenses", icon: "fa-regular fa-credit-card", badge: 2 }, { label: "Files", icon: "fa-regular fa-folder-open" }, ]; /* SuiDockItem[] */ return <SuiDock aria-label="Applications, Basic" orientation="horizontal" active="Home" items={items} onSuiClose={(e) => console.log(e.detail)} />; }
<script setup> import { SuiDock } from "@shineui/vue"; import "@shineui/core/shineui.css"; const items = [ { label: "Home", icon: "fa-solid fa-house" }, { label: "Mail", icon: "fa-regular fa-envelope", badge: 5 }, { label: "Expenses", icon: "fa-regular fa-credit-card", badge: 2 }, { label: "Files", icon: "fa-regular fa-folder-open" }, ]; // SuiDockItem[] const onChange = (e) => console.log(e.detail); </script> <template> <SuiDock aria-label="Applications, Basic" orientation="horizontal" active="Home" :items="items" @sui-close="onChange" /> </template>
import { Component } from "@angular/core"; import { SuiDock } from "@shineui/angular"; @Component({ standalone: true, imports: [SuiDock], template: `<sui-dock aria-label="Applications, Basic" orientation="horizontal" active="Home" [(collapsed)]="collapsed" [items]="items" (suiClose)="onChange($event)"></sui-dock>`, }) export class Example { collapsed = false; items = [ { label: "Home", icon: "fa-solid fa-house" }, { label: "Mail", icon: "fa-regular fa-envelope", badge: 5 }, { label: "Expenses", icon: "fa-regular fa-credit-card", badge: 2 }, { label: "Files", icon: "fa-regular fa-folder-open" }, ]; // SuiDockItem[] onChange(e: CustomEvent) { console.log(e.detail); } }

Basic

The dock is a navigation landmark named Applications; each tile is a button named by its label, the active one reads aria-current, and the grip control reads aria-expanded. Labels are drawn as a capsule on hover and on keyboard focus, so a Tab through the bar still names every app.

<sui-dock aria-label="Applications, Basic" orientation="horizontal" active="Home"></sui-dock>
// setup(root): properties and events setup(root) { root.querySelector('sui-dock').items = apps(); }

Orientations

Horizontal and vertical put the grip control first in the bar, in the same place the collapsed launcher sits, so the control never moves. The vertical bar draws its label capsule to the right.

<div style="display:flex;gap:40px;align-items:flex-start"> <sui-dock aria-label="Applications, Orientations: horizontal" id="dk-h" orientation="horizontal" active="Mail"></sui-dock> <sui-dock aria-label="Applications, Orientations: vertical" id="dk-v" orientation="vertical" active="Mail"></sui-dock> </div>
// setup(root): properties and events setup(root) { root.querySelector('#dk-h').items = apps(); root.querySelector('#dk-v').items = apps(); }

Radial fan

The radial center button is both launcher and closer, so it ignores no-collapse. direction aims the fan and radius is a preference: the arc widens first, then the radius grows, so satellites never overlap.

<div style="display:flex;gap:60px;align-items:flex-end;min-height:240px"> <div style="height:210px;display:flex;align-items:flex-end;padding-left:30px"> <sui-dock aria-label="Applications, Radial fan: upward" id="dk-up" orientation="radial" direction="up" radius="80" active="Home"></sui-dock> </div> <div style="height:210px;display:flex;align-items:center"> <sui-dock aria-label="Applications, Radial fan: rightward" id="dk-right" orientation="radial" direction="right" radius="80" arc="140" active="Files"></sui-dock> </div> </div>
// setup(root): properties and events setup(root) { root.querySelector('#dk-up').items = apps(); root.querySelector('#dk-right').items = apps(); }

A tile with href is a real link with the same capsule and indicator, so open in a new tab works and the browser follows it on Enter. suiNavigate still fires and active still updates on the click.

<sui-dock aria-label="Applications, Links" id="dk-links" orientation="horizontal" active="Home"></sui-dock>
// setup(root): properties and events setup(root) { root.querySelector('#dk-links').items = [ { label: 'Home', icon: 'fa-solid fa-house', href: '#/' }, { label: 'Mail', icon: 'fa-regular fa-envelope', href: '#/mail', badge: 5 }, { label: 'Files', icon: 'fa-regular fa-folder-open', href: '#/files' }, { label: 'Help center', icon: 'fa-regular fa-circle-question', href: 'https://example.com/help', target: '_blank' }, ]; }

Pinned to an edge

position pins the dock to a viewport edge, where it stays while the page scrolls; left and right run vertical, top and bottom horizontal. The label capsules open toward the page, so a right-hand dock names its tiles to their left. Only the bar takes presses; each box below uses contain: paint to hold its dock inside.

<div style="display:flex;gap:16px;flex-wrap:wrap"> <div style="contain:paint;position:relative;width:300px;height:300px;background:var(--surface-sunken);border:1px solid var(--border);border-radius:var(--radius-lg)"> <sui-dock aria-label="Applications, Pinned to an edge: bottom" id="dk-pin-bottom" position="bottom" active="Home"></sui-dock> </div> <div style="contain:paint;position:relative;width:300px;height:300px;background:var(--surface-sunken);border:1px solid var(--border);border-radius:var(--radius-lg)"> <sui-dock aria-label="Applications, Pinned to an edge: left" id="dk-pin-left" position="left" active="Mail"></sui-dock> </div> <div style="contain:paint;position:relative;width:300px;height:300px;background:var(--surface-sunken);border:1px solid var(--border);border-radius:var(--radius-lg)"> <sui-dock aria-label="Applications, Pinned to an edge: right" id="dk-pin-right" position="right" active="Expenses"></sui-dock> </div> <div style="contain:paint;position:relative;width:300px;height:300px;background:var(--surface-sunken);border:1px solid var(--border);border-radius:var(--radius-lg)"> <sui-dock aria-label="Applications, Pinned to an edge: top" id="dk-pin-top" position="top" active="Files"></sui-dock> </div> </div>
// setup(root): properties and events setup(root) { root.querySelector('#dk-pin-bottom').items = apps(); root.querySelector('#dk-pin-left').items = apps(); root.querySelector('#dk-pin-right').items = apps(); root.querySelector('#dk-pin-top').items = apps(); }

Collapsed

collapsed reduces a bar to its launcher tile and a radial fan to its closed center. Set no-collapse to remove the grip and keep the bar open for good.

<div style="display:flex;gap:40px;align-items:flex-start;min-height:130px"> <sui-dock aria-label="Applications, Collapsed: collapsible" id="dk-collapsed" orientation="horizontal" collapsed active="Home"></sui-dock> <sui-dock aria-label="Applications, Collapsed: fixed open" id="dk-fixed" orientation="horizontal" no-collapse active="Home"></sui-dock> </div>
// setup(root): properties and events setup(root) { root.querySelector('#dk-collapsed').items = apps(); root.querySelector('#dk-fixed').items = apps(); }

Events

Activating a tile that is not active writes active and emits suiActiveChange, then suiNavigate with its key, the item key if it has one, the label otherwise. The grip writes collapsed, so the dock emits suiOpen after it expands and suiClose before it collapses; preventDefault on suiClose keeps it open.

Open, on Home.
<div style="display:flex;gap:20px;align-items:center;min-height:110px"> <sui-dock aria-label="Applications, Events" id="dk-events" orientation="horizontal" active="Home"></sui-dock> <span id="dk-log" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Open, on Home.</span> </div>
// setup(root): properties and events setup(root) { const log = root.querySelector('#dk-log'); const el = root.querySelector('#dk-events'); el.items = apps(); el.addEventListener('suiNavigate', (e) => { log.textContent = `Opened ${e.detail}.`; }); el.addEventListener('suiOpen', () => { log.textContent = 'Dock expanded.'; }); el.addEventListener('suiClose', (e) => { log.textContent = `Collapsing, reason ${e.detail}.`; }); }

Keyboard

KeyAction
TabMoves through the collapse control, unless no-collapse leaves it out, then each tile. A tile with href is a link and one without is a button. Focus shows the same label capsule the pointer does, so a keyboard pass names every app.
EnterActivates the tile: emits suiActiveChange when it was not active, then suiNavigate with its key, and marks it aria-current; a link tile then follows its href. On the collapse control, collapses the dock after a cancelable suiClose, or expands it and emits suiOpen.
SpaceActivates a button tile or the collapse control. A link tile follows on Enter alone.

API

<sui-dock>

Properties

PropertyAttributeTypeDefaultDescription
activeactivestringstring | undefinedKey of the active app, its own key or its label. Mutable; updated on tile activation and reported through suiActiveChange.
arcarcnumbernumber | undefinedRadial: fan spread in degrees. Omit it and the fan picks its own spread, up to 300 degrees, then grows the radius so tiles never overlap. direction aims the fan.
collapsedcollapsedbooleanfalseCollapsed to the launcher tile, or the closed radial center. Mutable; toggled by the control.
directiondirection"down" | "left" | "right" | "up"'up'Radial only: where the fan opens. Reflected.
itemsproperty onlySuiDockItem[][]Tiles to render: { label, icon? | img?, badge?, href?, target? }[]. A tile with href is a real link. Property, not attribute.
noCollapseno-collapsebooleanfalseHorizontal and vertical only: leaves out the grip control that collapses the bar to a launcher, for a dock that always shows its tiles. Without it the grip sits first in the bar, where the collapsed launcher sits, so the control never moves. Radial ignores it: the center button is both launcher and closer. Reflected.
orientationorientation"horizontal" | "radial" | "vertical"'horizontal'Bar direction, or a radial fan around a center launcher. Ignored for the two bar directions while position is set, since the edge the dock is pinned to decides which way the bar runs.
positionposition"bottom" | "left" | "right" | "top" | undefinedPins the dock to an edge of the viewport, where it stays while the page scrolls. Unset (the default) it sits in the flow like any other element. A pinned bar runs along its edge (left and right vertical, top and bottom horizontal) and draws its label capsules toward the page, away from that edge; a radial dock keeps its fan and is pinned as it is. It sits at the sticky layer, under dropdowns, dialogs and toasts.
radiusradiusnumber74Radial: preferred satellite distance in px. It grows on its own when tiles would overlap.

Events

EventDetailReactVueDescription
suiActiveChangestringonSuiActiveChange@sui-active-changeFires whenever the element writes active, a press on a tile that was not active, before suiNavigate; detail is the new active. Writing active from script emits nothing. Does not bubble.
suiCloseSuiCloseReason"cancel" | "close-button" | "escape" | "outside" | "pick" | "scrim" | "trigger"onSuiClose@sui-closeFires before the dock collapses itself; detail is 'trigger' (its own control is the only way it collapses). Cancelable: preventDefault() keeps the dock open. It does not bubble.
suiNavigatestringonSuiNavigate@sui-navigateFires when a tile is activated; detail is the tile key, its own key or its label.
suiOpenvoidonSuiOpen@sui-openFires after the dock expands itself. It does not bubble.

Methods

MethodSignatureDescription
setFocus()setFocus() => Promise<void>Moves keyboard focus to the active tile, else the first tile, else the control that opens the dock while it is collapsed.

Types

SuiCloseReason typeWhy a dismissible surface is closing. Detail of the cancelable suiClose. A consumer expresses "a stray backdrop click must not close this" by calling preventDefault() on the reason it does not accept, which is why none of these needs a prop of its own.
No members.
SuiDockItem interfaceOne dock tile: label (also the tooltip), an icon class or a product-mark image, optional badge.
labelstring
key?stringIdentity for active and suiNavigate. Defaults to the label; set it once the labels are translated or edited, so a handler keys off something that does not move.
icon?string
img?string
badge?string | number
href?stringRenders the tile as a real link, so middle-click and open in a new tab work and Enter follows it. suiNavigate still fires and active still updates on the click.
target?stringLink target for a tile with href, e.g. _blank. A _blank tile gets rel="noopener".

Theming

Every design token this element can render with, read from its own stylesheets and the ones it imports. Set a token on :root or on any ancestor and every use below follows it. The values shown are the ones in the theme you are viewing. The list is read per stylesheet, so an element that imports a shared drawing module lists that module's tokens too, whether or not it paints with all of them.

See also