Signature
An animated SVG signature effect that draws out text as if hand-written.
Live preview recorded from Mellow UI; component and demo published by Mellow UI. Preview: platform recorded · rights authorized.
A macOS-style dock — icons swell under the cursor with a fisheye falloff, neighbours rising in sympathy, labels popping above on hover.
MSource LandMellow UIWhy it stands out
A macOS-style dock — icons swell under the cursor with a fisheye falloff, neighbours rising in sympathy, labels popping above on hover.
You are given a task to integrate a React component into your codebase.
Please verify your project has the following setup:
- shadcn/ui project structure
- Tailwind CSS v4.0
- TypeScript
- motion/react v11 (install: `pnpm add motion`)
If any of these are missing, provide instructions on how to setup project via shadcn CLI, install Tailwind or TypeScript.
Determine the default path for components and styles. Mellow components live in `components/mellow/`. If that folder does not exist yet, create it.
Copy-paste these files:
File location: components/fisheye-dock-demo.tsx
File content: "use client";
import React, { useEffect, useState } from "react";
import { FisheyeDock } from "../mellow/fisheye-dock";
function Icon({ d }: { d: string }) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.75"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d={d} />
</svg>
);
}
const items = [
{
icon: <Icon d="M3 10.5 12 3l9 7.5M5 9.5V21h5v-6h4v6h5V9.5" />,
label: "Home",
active: true,
},
{
icon: <Icon d="M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16Zm10 2-4.35-4.35" />,
label: "Search",
},
{
icon: <Icon d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6Zm0 0v6h6M9 13h6M9 17h6" />,
label: "Docs",
},
{
icon: <Icon d="M9 18V5l12-2v13M9 18a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm12-2a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />,
label: "Music",
},
{
icon: <Icon d="M21 15l-5-5L5 21M3 3h18v18H3V3Zm6 8a2 2 0 1 0 0-4 2 2 0 0 0 0 4Z" />,
label: "Gallery",
},
{
icon: <Icon d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6Zm7.4-3a7.4 7.4 0 0 0-.1-1.2l2-1.6-2-3.4-2.4 1a7.5 7.5 0 0 0-2-1.2L14.5 3h-5l-.4 2.6a7.5 7.5 0 0 0-2 1.2l-2.4-1-2 3.4 2 1.6a7.4 7.4 0 0 0 0 2.4l-2 1.6 2 3.4 2.4-1a7.5 7.5 0 0 0 2 1.2l.4 2.6h5l.4-2.6a7.5 7.5 0 0 0 2-1.2l2.4 1 2-3.4-2-1.6c.07-.4.1-.8.1-1.2Z" />,
label: "Settings",
},
];
/** The docs preview box is ~340px wide on phones — shrink to fit it. */
function useNarrow() {
const [narrow, setNarrow] = useState(false);
useEffect(() => {
const mq = window.matchMedia("(max-width: 640px)");
const sync = () => setNarrow(mq.matches);
sync();
mq.addEventListener("change", sync);
return () => mq.removeEventListener("change", sync);
}, []);
return narrow;
}
export default function FisheyeDockDemo() {
const narrow = useNarrow();
return (
<div className="flex flex-col items-center gap-6 p-4 sm:gap-10 sm:p-8">
<FisheyeDock
items={items}
size={narrow ? 36 : 44}
magnification={narrow ? 60 : 76}
distance={narrow ? 100 : 140}
/>
<p className="text-[0.8125rem] text-[rgba(var(--ink-rgb),0.35)]">
Sweep the cursor across the dock · click to select · hover for labels
</p>
</div>
);
}
File location: components/mellow/fisheye-dock.tsx
File content: "use client";
import React, { useRef, useState } from "react";
import {
AnimatePresence,
motion,
useMotionValue,
useReducedMotion,
useSpring,
useTransform,
type MotionValue,
} from "motion/react";
export interface FisheyeDockItem {
icon: React.ReactNode;
label: string;
href?: string;
onClick?: () => void;
/** Show the active indicator dot under the icon. */
active?: boolean;
}
export interface FisheyeDockProps {
items: FisheyeDockItem[];
/** Resting icon size in px. */
size?: number;
/** Icon size directly under the cursor in px. */
magnification?: number;
/** Cursor influence radius in px. */
distance?: number;
className?: string;
style?: React.CSSProperties;
}
/**
* A macOS-style dock — icons swell under the cursor with a fisheye falloff,
* neighbours rising in sympathy, labels popping above on hover.
*/
export function FisheyeDock({
items,
size = 44,
magnification = 76,
distance = 140,
className,
style,
}: FisheyeDockProps) {
const mouseX = useMotionValue(Infinity);
const reduced = useReducedMotion();
const [activeIndex, setActiveIndex] = useState(() =>
Math.max(0, items.findIndex((item) => item.active))
);
return (
<nav
aria-label="Dock"
onMouseMove={(e) => {
if (!reduced) mouseX.set(e.clientX);
}}
onMouseLeave={() => mouseX.set(Infinity)}
className={[
"flex items-end gap-2 rounded-2xl border border-[var(--rule)] bg-[rgba(var(--background-rgb),0.7)] px-2.5 pb-2.5 backdrop-blur-md",
className,
]
.filter(Boolean)
.join(" ")}
style={{ height: size + 20, ...style }}
>
{items.map((item, i) => (
<DockIcon
key={i}
item={item}
active={i === activeIndex}
onSelect={() => setActiveIndex(i)}
mouseX={mouseX}
size={size}
magnification={magnification}
distance={distance}
/>
))}
</nav>
);
}
function DockIcon({
item,
active,
onSelect,
mouseX,
size,
magnification,
distance,
}: {
item: FisheyeDockItem;
active: boolean;
onSelect: () => void;
mouseX: MotionValue<number>;
size: number;
magnification: number;
distance: number;
}) {
const ref = useRef<HTMLDivElement>(null);
const [hovered, setHovered] = useState(false);
const dist = useTransform(mouseX, (val: number) => {
const bounds = ref.current?.getBoundingClientRect() ?? { x: 0, width: 0 };
return val - bounds.x - bounds.width / 2;
});
const target = useTransform(
dist,
[-distance, 0, distance],
[size, magnification, size]
);
const side = useSpring(target, { mass: 0.1, stiffness: 170, damping: 13 });
const Comp: React.ElementType = item.href ? "a" : "button";
return (
<motion.div
ref={ref}
style={{ width: side, height: side }}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
className="relative flex items-center justify-center"
>
<AnimatePresence>
{hovered && (
<motion.span
initial={{ opacity: 0, y: 6, scale: 0.9 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 4, scale: 0.9 }}
transition={{ type: "spring", stiffness: 350, damping: 25 }}
style={{ x: "-50%" }}
className="pointer-events-none absolute -top-9 left-1/2 rounded-md border border-[var(--rule)] bg-[var(--background)] px-2 py-1 [font-family:var(--font-mono)] text-[0.625rem] font-medium tracking-[0.12em] whitespace-nowrap text-[var(--ink)] uppercase"
>
{item.label}
</motion.span>
)}
</AnimatePresence>
<Comp
href={item.href}
type={item.href ? undefined : "button"}
onClick={() => {
onSelect();
item.onClick?.();
}}
aria-label={item.label}
aria-current={active ? "page" : undefined}
className="flex h-full w-full cursor-pointer items-center justify-center rounded-xl border border-[rgba(var(--ink-rgb),0.1)] bg-[rgba(var(--ink-rgb),0.06)] text-[var(--ink)] transition-colors hover:bg-[rgba(var(--ink-rgb),0.1)] [&_svg]:h-[44%] [&_svg]:w-[44%]"
>
{item.icon}
</Comp>
{active && (
<motion.span
layoutId="fisheye-dock-active"
aria-hidden="true"
className="absolute -bottom-1.5 left-1/2 h-1 w-1 -translate-x-1/2 rounded-full bg-[oklch(0.65_0.25_250)]"
transition={{ type: "spring", stiffness: 380, damping: 28 }}
/>
)}
</motion.div>
);
}
export default FisheyeDock;
Usage notes:
Add a FisheyeDock component from the mellow library — a macOS-style dock nav where icons magnify under the cursor with spring physics and a fisheye falloff to their neighbours. Each item shows a mono uppercase tooltip label on hover and an accent active dot that slides to the clicked item (set `active: true` on one item for the initial selection). Pass an `items` array of { icon, label, href?, onClick?, active? } — icons are ReactNodes (SVGs scale to fit). Tune size, magnification, and distance for the magnify feel. Magnification is disabled under prefers-reduced-motion.Discovery vocabulary
Related by governed terms
An animated SVG signature effect that draws out text as if hand-written.
More from Mellow UI