Component Preview
'use client';
import {HugeiconsIcon} from '@hugeicons/react';
import useMeasure from 'react-use-measure';
import {AnimatePresence, motion, MotionConfig} from 'motion/react';
import {Activity, Flash, Search, User} from '@hugeicons/core-free-icons';
import {useEffect, useRef, useState} from 'react';
import {cn} from '@/lib/utils';
import Link from 'next/link';
import {Button} from '@/components/ui/button';
const transition = {
type: 'spring',
bounce: 0.1,
duration: 0.25,
};
const ITEMS = [
{
id: 1,
label: 'Profile',
title: <HugeiconsIcon icon={User} className="h-5 w-5" />,
content: (
<div className="flex flex-col gap-3">
<div className="flex items-center gap-3">
<div className="h-9 w-9 rounded-full bg-linear-to-br from-indigo-500 to-purple-500" />
<div className="text-sm">
<div className="font-medium text-zinc-800">Hems</div>
<div className="text-zinc-500 text-xs">Full-stack developer</div>
</div>
</div>
<Link href="/craft">
<Button variant="outline" className="w-full" size="sm">
Open dashboard
</Button>
</Link>
</div>
),
},
{
id: 2,
label: 'Search',
title: <HugeiconsIcon icon={Search} className="h-5 w-5" />,
content: (
<div className="flex flex-col gap-3">
<input
placeholder="Search components, docs..."
className="h-8 w-full rounded-md border px-2 text-sm outline-none focus:ring-2"
/>
<div className="text-xs text-zinc-500">Try: "buttons", "cards", "animations"</div>
</div>
),
},
{
id: 3,
label: 'Activity',
title: <HugeiconsIcon icon={Activity} className="h-5 w-5" />,
content: (
<div className="flex flex-col gap-2 text-sm text-zinc-600">
<div>Updated Navbar component</div>
<div>Pushed 3 commits</div>
<div>Fixed animation jitter</div>
<Button variant="outline" className="w-full mt-2" size="sm">
View history
</Button>
</div>
),
},
{
id: 4,
label: 'Quick Actions',
title: <HugeiconsIcon icon={Flash} className="h-5 w-5" />,
content: (
<div className="grid grid-cols-1 gap-2 text-xs">
<Button variant="outline" size="xs">
New Project
</Button>
<Button variant="outline" size="xs">
Add Component
</Button>
<Button variant="outline" size="xs">
Deploy
</Button>
<Button variant="outline" size="xs">
Settings
</Button>
</div>
),
},
];
function Toolbar() {
const [active, setActive] = useState<number | null>(null);
const [contentRef, {height: heightContent}] = useMeasure();
const [menuRef, {width: widthContainer}] = useMeasure();
const ref = useRef<HTMLDivElement>(null);
const [isOpen, setIsOpen] = useState(false);
const [maxWidth, setMaxWidth] = useState(0);
useEffect(() => {
const handleOutsideClick = (e: MouseEvent) => {
if (!ref.current?.contains(e.target as Node)) {
setIsOpen(false);
setActive(null);
}
};
window.addEventListener('mousedown', handleOutsideClick);
return () => window.removeEventListener('mousedown', handleOutsideClick);
}, []);
useEffect(() => {
if (!widthContainer || maxWidth > 0) return;
setMaxWidth(widthContainer);
}, [widthContainer, maxWidth]);
return (
<MotionConfig transition={transition as any}>
<div className="absolute bottom-8" ref={ref}>
<div className="h-full w-full rounded-xl border border-zinc-950/10 bg-white">
<div className="overflow-hidden">
<AnimatePresence initial={false} mode="sync">
{isOpen ? (
<motion.div
key="content"
initial={{height: 0}}
animate={{height: heightContent || 0}}
exit={{height: 0}}
style={{
width: maxWidth,
}}
>
<div ref={contentRef} className="p-2">
{ITEMS.map(item => {
const isSelected = active === item.id;
return (
<motion.div
key={item.id}
initial={{opacity: 0}}
animate={{opacity: isSelected ? 1 : 0}}
exit={{opacity: 0}}
>
<div className={cn('px-2 pt-2 text-sm', isSelected ? 'block' : 'hidden')}>
{item.content}
</div>
</motion.div>
);
})}
</div>
</motion.div>
) : null}
</AnimatePresence>
<div className="flex space-x-2 p-2" ref={menuRef}>
{ITEMS.map(item => (
<button
key={item.id}
aria-label={item.label}
className={cn(
'relative flex h-9 w-9 shrink-0 scale-100 select-none appearance-none items-center justify-center rounded-lg text-zinc-500 transition-colors hover:bg-zinc-100 hover:text-zinc-800 focus-visible:ring-2 active:scale-[0.98]',
active === item.id ? 'bg-zinc-100 text-zinc-800' : ''
)}
type="button"
onClick={() => {
if (!isOpen) setIsOpen(true);
if (active === item.id) {
setIsOpen(false);
setActive(null);
return;
}
setActive(item.id);
}}
>
{item.title}
</button>
))}
</div>
</div>
</div>
</div>
</MotionConfig>
);
}
export default Toolbar;
Toolbar
A compact floating toolbar with animated panels, built using Framer Motion. Supports multiple actions with a shared expandable panel, smooth transitions, and outside-click handling for a clean, responsive interaction model.
Framer MotionFloating UIToolbarPopoverAnimated PanelUI Patterns