Kosmesis
Components

Dock

A macOS-style magnifying dock, driven by pointer-distance DOM queries. Purely presentational — no Morphos equivalent.

Installation

npx kosmesis add dock
pnpm dlx kosmesis add dock
yarn dlx kosmesis add dock
bunx kosmesis add dock

Copy and paste the following code into your project.

dock.tsx
import { StatefulComponent, StatelessComponent } from "@praxisjs/core";import { Component, Prop, Ref, type Ref as RefType } from "@praxisjs/decorators";import type { Children } from "@praxisjs/shared";import { cn } from "@/lib/utils";/** Must match `DockItem`'s `size-11` default class — the base the pointer-driven magnify math scales up from. */const DOCK_ITEM_SIZE_PX = 44;export interface DockProps {  magnification?: number;  distance?: number;  class?: string;  id?: string;  children?: Children;}// Resizes real width/height in pixels rather than `transform: scale()`, which blurs the icon and// rounded corners in Safari once a layer is involved. `items-end` turns that resize into the// "lift"; the dock's own height is fixed (62px) so the rise pokes above the bar instead of growing it.@Component()export class Dock extends StatefulComponent {  @Prop() magnification = 1.5;  @Prop() distance = 140;  @Prop() class?: string;  @Prop() id?: string;  @Prop() children?: Children;  @Ref<HTMLDivElement>()  containerRef!: RefType<HTMLDivElement>;  // Captured once at rest, not re-read live: resizing an item during hover moves its neighbors,  // so recomputing from `getBoundingClientRect()` every pointermove chases a target that just  // moved — a feedback loop that reads as the whole row trembling.  private _restCenters: number[] = [];  onMount(): void {    this._captureRestCenters();    window.addEventListener("resize", this._captureRestCenters);  }  onUnmount(): void {    window.removeEventListener("resize", this._captureRestCenters);  }  private readonly _captureRestCenters = () => {    const container = this.containerRef.current;    if (!container) return;    this._restCenters = [...container.querySelectorAll<HTMLElement>("[data-slot=dock-item]")].map((item) => {      const rect = item.getBoundingClientRect();      return rect.left + rect.width / 2;    });  };  private readonly _handlePointerMove = (event: PointerEvent) => {    const container = this.containerRef.current;    if (!container) return;    container.querySelectorAll<HTMLElement>("[data-slot=dock-item]").forEach((item, index) => {      const center = this._restCenters.at(index);      if (center === undefined) return;      const dist = Math.abs(event.clientX - center);      const scale = dist < this.distance ? 1 + (this.magnification - 1) * (1 - dist / this.distance) : 1;      const size = `${String(DOCK_ITEM_SIZE_PX * scale)}px`;      item.style.width = size;      item.style.height = size;      // Read the icon's own width/height rather than a hardcoded constant, so this stays correct      // for icons that aren't the default 24px.      const icon = item.querySelector<SVGElement>("svg");      if (icon) {        const baseIconSize = Number(icon.getAttribute("width")) || 24;        const iconSize = `${String(baseIconSize * scale)}px`;        icon.style.width = iconSize;        icon.style.height = iconSize;      }    });  };  private readonly _handlePointerLeave = () => {    const container = this.containerRef.current;    if (!container) return;    for (const item of container.querySelectorAll<HTMLElement>("[data-slot=dock-item]")) {      item.style.width = "";      item.style.height = "";      const icon = item.querySelector<SVGElement>("svg");      if (icon) {        icon.style.width = "";        icon.style.height = "";      }    }  };  render() {    return (      <div        ref={this.containerRef}        id={this.id}        role="toolbar"        data-slot="dock"        class={cn("flex h-[62px] items-end gap-2 rounded-2xl border bg-popover p-2 shadow-lg", this.class)}        onPointerMove={this._handlePointerMove}        onPointerLeave={this._handlePointerLeave}      >        {this.children}      </div>    );  }}export interface DockItemProps {  label?: string;  class?: string;  children?: Children;}@Component()export class DockItem extends StatelessComponent<DockItemProps> {  render() {    const { label, class: cls, children } = this.props;    return (      <button        type="button"        aria-label={label}        data-slot="dock-item"        class={cn(          "flex size-11 shrink-0 items-center justify-center rounded-xl bg-muted text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground",          cls,        )}      >        {children}      </button>    );  }}

Install the following dependencies:

npm install @praxisjs/css
pnpm add @praxisjs/css
yarn add @praxisjs/css
bun add @praxisjs/css

Copy and paste the following code into your project.

dock.tsx
import { StatefulComponent, StatelessComponent } from "@praxisjs/core";import { cx, Stylesheet, Styled, tokenVars } from "@praxisjs/css";import { Component, Prop, Ref, type Ref as RefType } from "@praxisjs/decorators";import type { Children } from "@praxisjs/shared";import { KosmesisTokens } from "@/lib/kosmesis-theme";const t = tokenVars(KosmesisTokens);/** Must match `DockItem`'s `$item` width/height below — the base the pointer-driven magnify math scales up from. */const DOCK_ITEM_SIZE_PX = 44;class DockStyles extends Stylesheet {  $dock = this.css({    display: "flex",    height: "62px",    alignItems: "flex-end",    gap: "0.5rem",    borderRadius: "1rem",    border: `1px solid ${t.border}`,    backgroundColor: t.popover,    padding: "0.5rem",    boxShadow: "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)",  });  $item = this.css({    display: "flex",    height: "2.75rem",    width: "2.75rem",    flexShrink: 0,    alignItems: "center",    justifyContent: "center",    borderRadius: "0.75rem",    backgroundColor: t.muted,    color: t.mutedForeground,    transition: "background-color 150ms ease, color 150ms ease",  }).on("&:hover", { backgroundColor: t.accent, color: t.accentForeground });}export interface DockProps {  magnification?: number;  distance?: number;  class?: string;  id?: string;  children?: Children;}// Resizes real width/height in pixels rather than `transform: scale()`, which blurs the icon and// rounded corners in Safari once a layer is involved. `alignItems: "flex-end"` turns that resize// into the "lift"; the dock's own height is fixed (62px) so the rise pokes above the bar instead of growing it.@Component()export class Dock extends StatefulComponent {  @Styled(DockStyles) $s!: DockStyles;  @Prop() magnification = 1.5;  @Prop() distance = 140;  @Prop() class?: string;  @Prop() id?: string;  @Prop() children?: Children;  @Ref<HTMLDivElement>()  containerRef!: RefType<HTMLDivElement>;  // Captured once at rest, not re-read live: resizing an item during hover moves its neighbors,  // so recomputing from `getBoundingClientRect()` every pointermove chases a target that just  // moved — a feedback loop that reads as the whole row trembling.  private _restCenters: number[] = [];  onMount(): void {    this._captureRestCenters();    window.addEventListener("resize", this._captureRestCenters);  }  onUnmount(): void {    window.removeEventListener("resize", this._captureRestCenters);  }  private readonly _captureRestCenters = () => {    const container = this.containerRef.current;    if (!container) return;    this._restCenters = [...container.querySelectorAll<HTMLElement>("[data-slot=dock-item]")].map((item) => {      const rect = item.getBoundingClientRect();      return rect.left + rect.width / 2;    });  };  private readonly _handlePointerMove = (event: PointerEvent) => {    const container = this.containerRef.current;    if (!container) return;    container.querySelectorAll<HTMLElement>("[data-slot=dock-item]").forEach((item, index) => {      const center = this._restCenters.at(index);      if (center === undefined) return;      const dist = Math.abs(event.clientX - center);      const scale = dist < this.distance ? 1 + (this.magnification - 1) * (1 - dist / this.distance) : 1;      const size = `${String(DOCK_ITEM_SIZE_PX * scale)}px`;      item.style.width = size;      item.style.height = size;      // Read the icon's own width/height rather than a hardcoded constant, so this stays correct      // for icons that aren't the default 24px.      const icon = item.querySelector<SVGElement>("svg");      if (icon) {        const baseIconSize = Number(icon.getAttribute("width")) || 24;        const iconSize = `${String(baseIconSize * scale)}px`;        icon.style.width = iconSize;        icon.style.height = iconSize;      }    });  };  private readonly _handlePointerLeave = () => {    const container = this.containerRef.current;    if (!container) return;    for (const item of container.querySelectorAll<HTMLElement>("[data-slot=dock-item]")) {      item.style.width = "";      item.style.height = "";      const icon = item.querySelector<SVGElement>("svg");      if (icon) {        icon.style.width = "";        icon.style.height = "";      }    }  };  render() {    return (      <div        ref={this.containerRef}        id={this.id}        role="toolbar"        data-slot="dock"        class={cx(this.$s.$dock, this.class)}        onPointerMove={this._handlePointerMove}        onPointerLeave={this._handlePointerLeave}      >        {this.children}      </div>    );  }}export interface DockItemProps {  label?: string;  class?: string;  children?: Children;}@Component()export class DockItem extends StatelessComponent<DockItemProps> {  @Styled(DockStyles) $s!: DockStyles;  render() {    const { label, class: cls, children } = this.props;    return (      <button type="button" aria-label={label} data-slot="dock-item" class={cx(this.$s.$item, cls)}>        {children}      </button>    );  }}

Examples

About

Purely presentational — no Morphos equivalent. Rather than tracking per-item reactive state, onPointerMove walks the mounted [data-slot=dock-item] children directly (the same DOM-querying approach ResizableHandle uses) and sets each one's transform imperatively based on distance from the pointer.

Usage

import { Icon } from "@morphos/icons";

import { Dock, DockItem } from "@/components/ui/dock";

<Dock>
  <DockItem label="Home">
    <Icon name="House" />
  </DockItem>
  <DockItem label="Messages">
    <Icon name="MessageCircle" />
  </DockItem>
  <DockItem label="Calendar">
    <Icon name="Calendar" />
  </DockItem>
</Dock>

Props

Dock

PropTypeDefault
magnificationnumber1.5
distancenumber140 (px range of pointer influence)
classstring

DockItem

PropTypeDefault
labelstring
classstring

On this page