---
title: Dock
description: A launcher, what is open, and a corner. Search opens in the middle of the screen.
source: https://adamjurek.com/components/dock
---

## Home


<!-- ::start:showcase demo="dock" height="360" -->
<!-- ::end:showcase -->

## Three parts

| Part | Is |
| --- | --- |
| The search control | a pill with a magnifier. It opens a window on the desk |
| The tasks | one button per open window; pressing one raises it |
| The corner | whatever the consumer puts there. On this site: a reset, a LinkedIn link and a clock |

## It has no state

Every version of this that held its own open flag, its own query and its own
results grew a second way to do something the desk already did. What is left is
a row of buttons and three callbacks: raise this, close this, open search.


## Install

<!-- ::start:tabs -->

### TanStack

```shell
tanstack add https://adamjurek.com/r/tanstack/dock.json
```

### shadcn

```shell
pnpm dlx shadcn@latest add https://adamjurek.com/r/shadcn/dock.json
```

### pnpm

```shell
pnpm add @sushindustries/ui @sushindustries/atoms
```

<!-- ::end:tabs -->

## What you get

| | |
| --- | --- |
| Version | 0.1.0 |
| Category | layout · Page structure |
| Files | `dock.tsx` |
| Dependencies | None |
| Also installs | `icon` |
| Tags | block, navigation, search, no-js, no-deps |

> [!NOTE] No runtime dependencies
> It brings nothing with it beyond the stylesheet.

## Get Started


Install commands are on Home, attached from the registry - they are not written
here, because a second copy is a copy that goes stale. This tab starts after the
install worked.

## Use it

```tsx
import { Clock, Dock } from "@sushindustries/ui";

export function Example() {
	return (
		<Dock
			tasks={[
				{ id: "a", label: "Components", active: true },
				{ id: "b", label: "Search", icon: "search" },
			]}
			onSearch={() => {}}
			onSelectTask={(id) => {}}
			onCloseTask={(id) => {}}
			trailing={<Clock />}
		/>
	);
}
```

## What you should see

A strip: a round search well on the left if `onSearch` is set, one button per
task in the middle, and whatever `trailing` is on the right. Pressing a task
button that is already active minimises it instead of doing nothing - that
toggle is the whole interaction this component offers.

## If nothing happens

`Dock` renders unconditionally as long as it is mounted - there is no prop
that hides it. If pressing a task or the search well does nothing, check that
`onSelectTask` / `onSearch` are actually wired to something: this component
only calls the callback with the task's `id`, it never decides what pressing
a task should do.


## Guides


## Search opens a window, not a panel

The control is one glyph in a round well. It was a pill with the word Search in
it, which put a labelled button beside a row of buttons labelled with folder
names, all competing for the same reading. A magnifier is the one icon that
needs no label; the word lives in the tooltip and the `aria-label`.

Pressing it does not open anything here. It calls `onSearch`, and on a desktop
that means opening a window - dragged, resized, raised, closed and remembered by
exactly the same code as a folder, because it is the same thing.

That is the third version. The first was a panel above the button, which put the
results in the corner you were already looking away from and opened off the edge
on a narrow screen. The second was a palette centred on the screen, which was
better placed and still a second kind of surface on a desktop that already had
one - and which arrived with its own bugs about stacking contexts and its own
missing height bound.

A search window has none of those problems, because none of them are its
problems. It inherits the answers a window already has.

> [!CAUTION] A stacking context is not a containing block
> The dock needs to be above the desktop so a dragged window cannot cover it.
> Doing that with `position: relative; z-index` also made it the containing
> block for what it contained - which is how the centred palette ended up
> centring itself on a forty-pixel strip. A flex item takes a `z-index` while
> staying statically positioned, so `z-index` **alone** lifts the dock without
> capturing anything inside it.

## Tasks toggle

A tab bar implies one of them is showing and the others are not. Here they are
all on screen at once, stacked.

Pressing one is a toggle, which is the behaviour every taskbar has and almost
nobody writes down:

| The window is | Pressing its task |
| --- | --- |
| minimised | brings it back, and to the front |
| behind another | brings it to the front |
| already in front | minimises it |

The third case is the one people find by accident and then use constantly. It
is also why this is `toggle` in `useDeskState` rather than `raise` - the dock
does not decide, it presses.

Each task has its own close button, so a window can be dismissed without being
raised first. Minimised tasks are dimmed rather than hidden: a window that
vanishes from the dock when minimised is a window somebody has lost.

The row scrolls sideways rather than wrapping. A dock that grows a second row
moves the desktop above it, and a desktop that resizes because you opened a
window is a desktop that loses track of your icons.

## Embedded, not applied

An inset shadow at the top edge and a fill darker than the desktop, so the light
falls *into* the dock rather than off the front of it. That is the whole
difference between a strip laid on top and a channel cut in.

```css
.dock {
	background: color-mix(in srgb, var(--bg-3) 86%, transparent);
	box-shadow:
		inset 0 1px 0 color-mix(in srgb, var(--bg-3) 90%, transparent),
		inset 0 2px 6px color-mix(in srgb, var(--bg-3) 60%, transparent);
}
```

No blur: it sits on the desktop, which is a solid surface, so there is nothing
behind it worth a GPU readback. Controls inside it have no borders either - the
channel is already a boundary, and a border inside it is a second one saying the
same thing.

## Where this is used

| Where | Doing |
| --- | --- |
| The home page laptop | via `Laptop`'s `dock` slot |
| `site-shelf.tsx` | opens `SEARCH_PATH` on the desk, and decides what a chosen result does |

It is a child of the screen rather than of the scrolling desktop, so it stays
put while the desktop scrolls under it.


## API


<!-- generated:api -->

## Props

| Prop | Type | Default | Does |
| --- | --- | --- | --- |
| `tasks?` | `readonly DockTask[]` | `[]` | What is open. One button each; pressing one brings it to the front. |
| `label?` | `string` | `"Search"` | Text on the search control. |
| `trailing?` | `ReactNode` | - | Right-hand side. A count, a clock, a link. |

<!-- /generated:api -->

## Notes

`onSearch` is the flag for the search well, the same way `onCloseTask` is the
flag for the per-task close button: leave either unset and that control does
not render, rather than rendering disabled. `label` only has an effect when
`onSearch` is set - it is the accessible name and tooltip for a control that
does not exist otherwise.

`tasks` with no entries renders an empty row rather than nothing, since the
search well and `trailing` may still have something to show.


## Examples


Examples are the tab where the component is shown doing a job, not
demonstrating a prop. The API tab already lists the props.

<!-- ::start:showcase demo="dock" height="420" -->
<!-- ::end:showcase -->

Press Compare. The frames are real viewports, so a layout that breaks at 320
breaks here too rather than in somebody's hands.

## In a page

```tsx
import { Clock, Dock } from "@sushindustries/ui";
import { useDeskState } from "./use-desk-state";

export function ScreenDock() {
	const { tasks, toggle, close, openSearch } = useDeskState();

	return (
		<Dock
			tasks={tasks}
			onSelectTask={toggle}
			onCloseTask={close}
			onSearch={openSearch}
			trailing={<Clock />}
		/>
	);
}
```

## What this example is not

Not proof that `Dock` remembers anything. `tasks` and the three callbacks
all come from `useDeskState` here - `Dock` itself holds no state, so a page
that skips a real desk hook and hardcodes `tasks` gets buttons that never
change what they show.
