Style Library
Toolbar Dynamic
AnimationanimationChanges height based on content, expanding to display more tools and shrinking to save space. Keeps your screen organized and efficient.
Prompt
Paste into any AI assistant before describing what to build — or attach it to a project below.
# Animation pattern — Toolbar Dynamic
Changes height based on content, expanding to display more tools and shrinking to save space. Keeps your screen organized and efficient.
Build it in React with `motion` (motion/react) and Tailwind CSS. Respect `prefers-reduced-motion` (render the final state without movement), keep it under ~400 ms for UI feedback, and animate only transform and opacity where possible.
Reference implementation (Motion-Primitives, MIT):
```tsx
'use client';
import React, { useEffect, useRef, useState } from 'react';
import useMeasure from 'react-use-measure';
import { AnimatePresence, motion, MotionConfig } from 'motion/react';
import { cn } from '@/lib/utils';
import useClickOutside from '@/hooks/useClickOutside';
import { Folder, MessageCircle, User, WalletCards } from 'lucide-react';
const transition = {
type: 'spring',
bounce: 0.1,
duration: 0.25,
};
const ITEMS = [
{
id: 1,
label: 'User',
title: <User className='h-5 w-5' />,
content: (
<div className='flex flex-col space-y-4'>
<div className='flex flex-col space-y-1 text-zinc-700'>
<div className='h-8 w-8 rounded-full bg-linear-to-br from-blue-500 to-blue-400' />
<span>Ibelick</span>
</div>
<button
className='relative h-8 w-full scale-100 select-none appearance-none items-center justify-center rounded-lg border border-zinc-950/10 px-2 text-sm text-zinc-500 transition-colors hover:bg-zinc-100 hover:text-zinc-800 focus-visible:ring-2 active:scale-[0.98]'
type='button'
>
Edit Profile
</button>
</div>
),
},
{
id: 2,
label: 'Messages',
title: <MessageCircle className='h-5 w-5' />,
content: (
<div className='flex flex-col space-y-4'>
<div className='text-zinc-700'>You have 3 new messages.</div>
<button
className='relative h-8 w-full scale-100 select-none appearance-none items-center justify-center rounded-lg border border-zinc-950/10 px-2 text-sm text-zinc-500 transition-colors hover:bg-zinc-100 hover:text-zinc-800 focus-visible:ring-2 active:scale-[0.98]'
type='button'
>
View more
</button>
</div>
),
},
{
id: 3,
label: 'Documents',
title: <Folder className='h-5 w-5' />,
content: (
<div className='flex flex-col space-y-4'>
<div className='flex flex-col text-zinc-700'>
<div className='space-y-1'>
<div>Project_Proposal.pdf</div>
<div>Meeting_Notes.docx</div>
<div>Financial_Report.xls</div>
</div>
</div>
<button
className='relative h-8 w-full scale-100 select-none appearance-none items-center justify-center rounded-lg border border-zinc-950/10 px-2 text-sm text-zinc-500 transition-colors hover:bg-zinc-100 hover:text-zinc-800 focus-visible:ring-2 active:scale-[0.98]'
type='button'
>
Manage documents
</button>
</div>
),
},
{
id: 4,
label: 'Wallet',
title: <WalletCards className='h-5 w-5' />,
content: (
<div className='flex flex-col space-y-4'>
<div className='flex flex-col text-zinc-700'>
<span>Current Balance</span>
<span>$1,250.32</span>
</div>
<button
className='relative h-8 w-full scale-100 select-none appearance-none items-center justify-center rounded-lg border border-zinc-950/10 px-2 text-sm text-zinc-500 transition-colors hover:bg-zinc-100 hover:text-zinc-800 focus-visible:ring-2 active:scale-[0.98]'
type='button'
>
View Transactions
</button>
</div>
),
},
];
export default function ToolbarExpandable() {
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);
useClickOutside(ref, () => {
setIsOpen(false);
setActive(null);
});
useEffect(() => {
if (!widthContainer || maxWidth > 0) return;
setMaxWidth(widthContainer);
}, [widthContainer, maxWidth]);
return (
<MotionConfig transition={transition}>
<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>
<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>
</MotionConfig>
);
}
```Use it in an app
Create a free account to attach this style to a project: the hub adds it to the prompt your AI assistant gets, so it builds every page in this look.
From Motion-Primitives (MIT, commit 120f64f).View source