Skip to content
omaris

Navigation

Dock

The macOS dock, in Material.

import { Dock } from 'omaris'
Learn

Examples

Basic

Move the pointer along the bar: the tile under it swells, its neighbours less, and the name rises over it. On a touch screen the tiles stay put at a finger's size; from the keyboard the arrow keys walk them.

<script lang="ts">	import { Dock, DockItem, DockDivider } from 'omaris';	import Icon from '$lib/patterns/shell/icon.svelte';​	const APPS = [		{ name: 'Home', icon: 'home' },		{ name: 'Mail', icon: 'mail', badge: 3 },		{ name: 'Calendar', icon: 'calendar' },		{ name: 'Messages', icon: 'message' },		{ name: 'Music', icon: 'music' },		{ name: 'Settings', icon: 'settings' }	] as const;​	let current = $state('Home');</script>​<div class="flex w-full flex-1 items-end justify-center">	<Dock label="Apps">		{#each APPS as app (app.name)}			<DockItem				label={app.name}				active={current === app.name}				badge={'badge' in app ? app.badge : undefined}				onclick={() => (current = app.name)}			>				<Icon name={app.icon} />			</DockItem>		{/each}		<DockDivider />		<DockItem label="Trash"><Icon name="trash" /></DockItem>	</Dock></div>

Vertical

Down the side, the way a rail sits. Labels ride out beside the tiles.

<script lang="ts">	import { Dock, DockItem } from 'omaris';	import Icon from '$lib/patterns/shell/icon.svelte';​	const TOOLS = [		{ name: 'Select', icon: 'target' },		{ name: 'Draw', icon: 'edit' },		{ name: 'Crop', icon: 'crop' },		{ name: 'Adjust', icon: 'sliders' },		{ name: 'Magic', icon: 'wand' }	] as const;​	let tool = $state('Select');</script>​<div class="flex w-full flex-1 items-center">	<Dock orientation="vertical" label="Tools" variant="filled">		{#each TOOLS as item (item.name)}			<DockItem label={item.name} active={tool === item.name} onclick={() => (tool = item.name)}>				<Icon name={item.icon} />			</DockItem>		{/each}	</Dock></div>

Glass

variant="glass" sets the tiles on frosted glass: what is behind the bar blurs through it.

<script lang="ts">	import { Dock, DockItem } from 'omaris';	import Icon from '$lib/patterns/shell/icon.svelte';​	const APPS = [		{ name: 'Home', icon: 'home' },		{ name: 'Photos', icon: 'image' },		{ name: 'Camera', icon: 'camera' },		{ name: 'Maps', icon: 'navigation' },		{ name: 'Music', icon: 'music' },		{ name: 'Settings', icon: 'settings' }	] as const;</script>​<div	class="relative flex min-h-72 w-full items-end justify-center overflow-hidden rounded-shape-xl bg-linear-to-br from-primary via-tertiary to-warning p-6">	<div class="absolute inset-x-8 top-8 grid grid-cols-3 gap-3">		{#each Array.from({ length: 6 }, (_, i) => i) as cell (cell)}			<div class="h-12 rounded-shape-md bg-background/35"></div>		{/each}	</div>	<Dock variant="glass" label="Apps">		{#each APPS as app (app.name)}			<DockItem label={app.name} active={app.name === 'Home'}>				<Icon name={app.icon} />			</DockItem>		{/each}	</Dock></div>

Overridden

The overridden case: a bigger base tile, a square dark bar and tiles restyled through classes — the dock still owns their size, so only the look changes.

<script lang="ts">	import { Dock, DockItem } from 'omaris';	import Icon from '$lib/patterns/shell/icon.svelte';​	const APPS = [		{ name: 'Home', icon: 'home' },		{ name: 'Search', icon: 'search' },		{ name: 'Library', icon: 'layers' },		{ name: 'Profile', icon: 'user' }	] as const;</script>​<div class="flex w-full flex-1 items-end justify-center">	<Dock		size={56}		magnification={1.5}		distance={110}		label="Apps"		classes={{ surface: 'rounded-shape-lg bg-inverse-surface ring-0' }}	>		{#each APPS as app (app.name)}			<DockItem				label={app.name}				active={app.name === 'Home'}				classes={{					control:						'rounded-shape-md bg-inverse-surface text-inverse-surface-foreground ring-1 ring-inverse-surface-foreground/15 ring-inset',					dot: 'bg-inverse-surface-foreground'				}}			>				<Icon name={app.icon} />			</DockItem>		{/each}	</Dock></div>

When to use it

Use it for

  • A launcher: a handful of apps, tools or workspaces a person switches between all day. active marks the one that is running or current.
  • A floating toolbar over a canvas, a map or a board: orientation="vertical" down one side, variant="glass" when it sits over the picture.
  • Quick actions in a desktop dashboard's corner, where a pointer will find them and a swelling tile makes six icons easy to tell apart.
  • A shortcut strip with counts: badge for what is waiting behind a tile.

Not for

  • The main navigation of a phone app → Navigation Bar. A finger gets no magnification, and a bar that names its destinations is what a phone needs.
  • More than about eight items → Navigation Drawer or a Menu. A dock that scrolls is a list.
  • Actions that belong to one screen's content → Button in the content, or a FAB for the one that matters.
  • A row of options where one is chosen → Segmented Button.

Do

  • Give it a label ("Apps"), and every DockItem a label — it is the tooltip and the accessible name, and an icon alone names nothing.
  • Keep the icons one family and one weight. The tiles are identical boxes; a mismatched icon is the only thing on the bar that will look wrong.
  • Leave size at 48 on anything a finger might use. A dock does not swell for a touch, so the base size is the whole target.
  • Use variant="glass" only over something worth blurring — a photo, a gradient, a map. Over a flat surface it is elevated for more work.
  • Put a DockDivider between the things that launch and the things that collect — apps, then the trash.

Don't

  • Set a width, height or transform on a DockItem from outside. The dock owns a tile's size and place; style the tile through classes.control, and the bar's look through classes.surface — bar is only the row.
  • Raise magnification past about 2. Tiles that double push their neighbours out of reach and the pointer chases a moving target.
  • Use labels="never" on a dock of abstract icons. It is for a dock whose icons are the brand marks everyone knows.
  • Put a dock inside a scrolling container that clips it. The tiles grow out of the bar, and a clipped tile reads as a bug.

Quick reference

orientation Dock
  • horizontal (default)
  • vertical
variant Dock
  • elevated (default)
  • filled
  • glass
  • plain
orientation DockDivider
  • horizontal (default)
  • vertical
orientation DockItem
  • horizontal (default)
  • vertical
labels DockItem
  • hover (default)
  • always
  • never

How the label is shown.

API

Dock

The macOS dock, in Material.

A row of tiles that swell under the pointer — the one nearest it most, its neighbours less, the rest not at all — and shrink back the moment it leaves. Every tile is a real button or link, so it is a launcher, a toolbar, a quick-actions strip: anything a handful of icons can name.

The motion. Every tile's scale is a spring chasing a target set by its distance from the pointer — a cosine bump, distance px wide, up to magnification times the base size. The swell is drawn with transforms, never layout: the neighbours slide aside by exactly what the tiles grew, around the pointer, so the tile under it stays under it and the bar widens out of both ends. Distances are measured on the row at rest, which does not move, so there is nothing for the motion to feed back on — no flicker on a fast sweep, no drift at the ends. The bar's own height never changes; the tiles rise out of it. One frame loop writes it all, and only while something is still moving.

A finger gets no magnification. There is no pointer to be near on a touch screen, and a tile that grows under a thumb moves the thing the thumb is about to press. The tiles stay at size, which is 48px by default — a comfortable target — and the press itself is answered by the state layer and the ripple.

A keyboard gets it. Tab into the dock, the arrow keys move between tiles, and the tile with focus is the one that swells, label showing, so the keyboard sees exactly what the pointer sees.

It fits. A row of eight tiles at 48px is wider than a small phone. Rather than scroll — a dock that scrolls is a list — the tiles shrink, evenly, until the bar fits the width it has, and grow back when it has more. A row that had to shrink has no room to swell either, so it holds still. size is the most a tile gets, not a promise.

import { Dock } from 'omaris'
<Dock label="Apps">  <DockItem label="Mail" active onclick={openMail}>{@render mailIcon()}</DockItem>  <DockItem label="Calendar" badge="3">{@render calendarIcon()}</DockItem>  <DockDivider />  <DockItem label="Trash">{@render trashIcon()}</DockItem></Dock>

Parts

Every element this renders is reachable from outside. Pass Tailwind for one part as classes={{ part: '…' }}; class covers the root and is merged last, so it always wins.

bar
The row the tiles sit in. Its size is the dock at rest, always.
surface
What the bar looks like: a layer behind the tiles that stretches with them as they swell, so style this, not bar.

Props

orientation

Defaults to 'horizontal'

DockOrientation
horizontal
vertical
variant

Defaults to 'elevated'

DockVariant
elevated
A tonal MD3 surface with a soft edge.
filled
The same, flatter, for a dock that sits inside a card.
glass
Frosted glass: whatever is behind the bar blurs through it.
plain
No bar at all: tiles on whatever is behind them.
size

Defaults to 48

number

Base tile size, in px.

magnification

Defaults to 1.7

number

How big the tile under the pointer gets, as a multiple of size. 1 turns the effect off.

distance

Defaults to 130

number

How far from the pointer a tile is still affected, in px.

labels

Defaults to 'hover'

DockLabels

When labels show. hover (the default) is the tooltip above the tile under the pointer or with focus; always prints them under every tile; never keeps them for assistive tech only.

label
string

Accessible name — "Apps".

class
string
classes
DockClasses

Per-part Tailwind overrides. class still covers the root.

children
Snippet

DockDivider

A hairline between two groups of tiles — apps on one side, the trash on the other.

import { DockDivider } from 'omaris'

Props

class
string

DockItem

One tile in a Dock: an icon, a name, and the press.

A button, or a link when it has an href. The name is the tooltip that rises over the tile as it swells, and the accessible name whether or not it is ever drawn. active puts the running-app dot under it; badge the count in its corner.

The tile's size and place belong to the dock: it sets the rest size as --dock-base and, frame by frame, the swell as --dock-scale and the slide aside as the root's translate — so style the control, not its box, and size the icon relative to it ([&_svg]:size-1/2 is already set).

import { DockItem } from 'omaris'

Parts

Every element this renders is reachable from outside. Pass Tailwind for one part as classes={{ part: '…' }}; class covers the root and is merged last, so it always wins.

root
The tile's place in the row, at rest. The label hangs off it.
tile
The tile, at the dock's base size, swollen by a transform so the row's layout never moves. Holds the control and the badge.
control
The pressable tile itself.
label
The name, as a tooltip or a caption.
dot
The dot that says "running" — or "current".
badge
The count in the corner.

Props

label required
string

The tile's name. Drawn as the tooltip, and read to assistive tech always.

href
string

Makes it a link.

target
string
active

Defaults to false

boolean

The running dot under the tile — or "you are here".

badge
string | number | boolean

A count in the corner. true for a plain dot.

disabled

Defaults to false

boolean
onclick
(event: MouseEvent) => void
class
string
classes
DockItemClasses

Per-part Tailwind overrides. class still covers the root.

children
Snippet

The icon. An <svg> is sized to half the tile, an <img> fills it.