Navigation
Dock
The macOS dock, in Material.
import { Dock } from 'omaris' 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> Labels and links
labels="always" prints the names under the tiles, href makes a tile a link, and magnification={1} keeps the bar still for a toolbar that is pressed more than admired.
<script lang="ts"> import { Dock, DockItem } from 'omaris'; import Icon from '$lib/patterns/shell/icon.svelte';</script><div class="flex w-full flex-1 items-end justify-center"> <Dock labels="always" magnification={1} variant="filled" label="Shortcuts"> <DockItem label="Repo" href="https://github.com" target="_blank" ><Icon name="github" /></DockItem > <DockItem label="Docs" href="/docs/components/dock" active><Icon name="file" /></DockItem> <DockItem label="Blocks" href="/blocks" badge><Icon name="activity" /></DockItem> <DockItem label="Learn" href="/learn" badge={2}><Icon name="rocket" /></DockItem> </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.
activemarks 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:
badgefor 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 everyDockItemalabel— 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
sizeat 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 iselevatedfor more work. - Put a
DockDividerbetween the things that launch and the things that collect — apps, then the trash.
Don't
- Set a width, height or transform on a
DockItemfrom outside. The dock owns a tile's size and place; style the tile throughclasses.control, and the bar's look throughclasses.surface—baris only the row. - Raise
magnificationpast 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 Dockhorizontal(default)vertical
variant Dockelevated(default)filledglassplain
orientation DockDividerhorizontal(default)vertical
orientation DockItemhorizontal(default)vertical
labels DockItemhover(default)alwaysnever
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 horizontalvertical
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.