Motion Primitives
uitoolbar-dynamicsource: ok · hit
Toolbar Dynamic
A toolbar component with dynamic appearance based on user interaction.
npx shadcn add "https://motion-primitives.com/c/toolbar-dynamic.json"free
Interaction
scroll-reveal
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · mediumanimationmotion
Files2 · source cached 10/8/2026, 3:38:18 PM
toolbar-dynamic.tsx
'use client';
import React, { useRef, useState } from 'react';
import { motion, MotionConfig } from 'motion/react';
import useClickOutside from '@/hooks/useClickOutside';
import { ArrowLeft, Search, User } from 'lucide-react';
const transition = {
type: 'spring',
bounce: 0.1,
duration: 0.2,
};
function Button({
children,
onClick,
disabled,
ariaLabel,
}: {
children: React.ReactNode;
onClick?: () => void;
disabled?: boolean;
ariaLabel?: string;
}) {
return (
<button
className='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] disabled:pointer-events-none disabled:opacity-50'
type='button'
onClick={onClick}
disabled={disabled}
aria-label={ariaLabel}
>
{children}
</button>
);
}
export default function ToolbarDynamic() {
const [isOpen, setIsOpen] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
useClickOutside(containerRef, () => {
setIsOpen(false);
});
return (
<MotionConfig transition={transition}>
<div className='absolute bottom-8' ref={containerRef}>
<div className='h-full w-full rounded-xl border border-zinc-950/10 bg-white'>
<motion.div
animate={{
// @todo: here I want to remove the width
width: isOpen ? '300px' : '98px',
}}
initial={false}
>
<div className='overflow-hidden p-2'>
{!isOpen ? (
<div className='flex space-x-2'>
<Button disabled ariaLabel='User profile'>
<User className='h-5 w-5' />
</Button>
<Button
onClick={() => setIsOpen(true)}
ariaLabel='Search notes'
>
<Search className='h-5 w-5' />
</Button>
</div>
) : (
<div className='flex space-x-2'>
<Button onClick={() => setIsOpen(false)} ariaLabel='Back'>
<ArrowLeft className='h-5 w-5' />
</Button>
<div className='relative w-full'>
<input
className='h-9 w-full rounded-lg border border-zinc-950/10 bg-transparent p-2 text-zinc-900 placeholder-zinc-500 focus:outline-hidden'
autoFocus
placeholder='Search notes'
/>
<div className='absolute right-1 top-0 flex h-full items-center justify-center'></div>
</div>
</div>
)}
</div>
</motion.div>
</div>
</div>
</MotionConfig>
);
}
hooks/useClickOutside.tsx
import { RefObject, useEffect } from 'react';
function useClickOutside<T extends HTMLElement>(
ref: RefObject<T>,
handler: (event: MouseEvent | TouchEvent) => void
): void {
useEffect(() => {
const handleClickOutside = (event: MouseEvent | TouchEvent) => {
if (!ref || !ref.current || ref.current.contains(event.target as Node)) {
return;
}
handler(event);
};
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('touchstart', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('touchstart', handleClickOutside);
};
}, [ref, handler]);
}
export default useClickOutside;