Please use Desktop to view and interact with components
Microinteractions300msspring
Morphing Action Button
A circular FAB that morphs into a rectangular action panel using layoutId
fabmorphlayoutbuttonaction
Useto navigate between components
Preview
Click the button to see morph animation
Code
TypeScript + React
'use client'
import { useState } from 'react'
import { motion, AnimatePresence, LayoutGroup } from 'framer-motion'
import { Plus, X } from 'lucide-react'
export function MorphingActionButton() {
const [isExpanded, setIsExpanded] = useState(false)
return (
<div className="fixed bottom-8 right-8 z-50">
<LayoutGroup>
<motion.div
layout
className="relative"
initial={false}
animate={{
width: isExpanded ? 280 : 56,
height: isExpanded ? 'auto' : 56,
borderRadius: isExpanded ? 16 : 28,
}}
transition={{
type: 'spring',
stiffness: 300,
damping: 30,
}}
>
<motion.button
onClick={() => setIsExpanded(!isExpanded)}
className="absolute right-0 bottom-0 flex h-14 w-14 items-center justify-center rounded-full bg-primary text-primary-foreground shadow-lg"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
layoutId="fab-button"
>
<AnimatePresence mode="wait">
{isExpanded ? (
<motion.div
key="close"
initial={{ rotate: -90, opacity: 0 }}
animate={{ rotate: 0, opacity: 1 }}
exit={{ rotate: 90, opacity: 0 }}
>
<X className="h-5 w-5" />
</motion.div>
) : (
<motion.div
key="plus"
initial={{ rotate: 90, opacity: 0 }}
animate={{ rotate: 0, opacity: 1 }}
exit={{ rotate: -90, opacity: 0 }}
>
<Plus className="h-5 w-5" />
</motion.div>
)}
</AnimatePresence>
</motion.button>
<AnimatePresence>
{isExpanded && (
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
className="absolute bottom-0 right-0 w-64 rounded-2xl border border-border bg-card p-4 shadow-2xl"
layoutId="fab-panel"
>
<div className="mb-2 space-y-2">
{['New Task', 'New Project', 'New Team'].map((label, index) => (
<motion.button
key={label}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: index * 0.05 + 0.2 }}
onClick={() => setIsExpanded(false)}
className="flex w-full items-center gap-3 rounded-lg px-4 py-3 text-left text-sm transition-colors hover:bg-muted"
>
<Plus className="h-4 w-4 text-muted-foreground" />
<span className="font-medium">{label}</span>
</motion.button>
))}
</div>
</motion.div>
)}
</AnimatePresence>
</motion.div>
</LayoutGroup>
</div>
)
}
How to Use
- 1Install Framer Motion:
npm install framer-motion - 2Copy the code from above
- 3Paste it into your project and customize as needed
- 4Colors are customizable via Tailwind CSS classes. The default theme uses dark mode colors defined in your globals.css file