Folder Flow
An animated folder icon with papers that rise smoothly on hover.
Preview
Installation
pnpm dlx shadcn@latest add https://makeui.dev/r/folder-flow.jsonDependencies
- motion
- lucide-react
Interaction
Hover over the folder to animate the papers rising out of it. Move the pointer away to smoothly return the papers to their original position. It also opens on keyboard focus and when tapped on touch screens.
Props
| Prop | Type | Description |
|---|---|---|
| className | string | Extra classes merged onto the root element. |
How to use
import FolderFlow from "@/components/ui/folder-flow"
export function Example() {
return <FolderFlow />
}Source code
components/ui/folder-flow.tsx
"use client"
import { motion, type Variants } from "motion/react"
import { FileText, Image as ImageIcon, Music } from "lucide-react"
import { cn } from "@/lib/utils"
const spring = { type: "spring", stiffness: 260, damping: 22 } as const
const papers = [
{
Icon: ImageIcon,
rest: { rotate: -4, x: -14, y: 0 },
hover: { rotate: -12, x: -34, y: -46 },
},
{ Icon: FileText, rest: { rotate: 0, x: 0, y: 0 }, hover: { rotate: 0, x: 0, y: -58 } },
{ Icon: Music, rest: { rotate: 4, x: 14, y: 0 }, hover: { rotate: 12, x: 34, y: -46 } },
]
const front: Variants = {
rest: { rotateX: 0 },
hover: { rotateX: -22, transition: spring },
}
export default function FolderFlow({ className }: { className?: string }) {
return (
<motion.div
initial="rest"
animate="rest"
whileHover="hover"
whileFocus="hover"
whileTap="hover"
tabIndex={0}
aria-label="Folder"
className={cn(
"relative mx-auto h-36 w-44 cursor-pointer rounded-xl outline-none [perspective:700px]",
className,
)}
>
{/* back panel with tab */}
<div className="absolute inset-x-0 bottom-0 h-28 rounded-xl bg-foreground/15">
<div className="absolute -top-3 left-0 h-4 w-16 rounded-t-lg bg-foreground/15" />
</div>
{papers.map(({ Icon, rest, hover }, i) => (
<motion.div
key={i}
variants={{
rest: { ...rest, transition: spring },
hover: { ...hover, transition: { ...spring, delay: i * 0.03 } },
}}
className="absolute inset-x-6 bottom-3 grid h-24 place-items-center rounded-md border border-border bg-card shadow-sm"
style={{ zIndex: i === 1 ? 2 : 1 }}
>
<Icon className="size-5 text-muted-foreground" />
<span className="absolute inset-x-3 top-3 h-1 rounded-full bg-muted" />
<span className="absolute inset-x-3 top-6 h-1 w-1/2 rounded-full bg-muted" />
</motion.div>
))}
{/* front panel */}
<motion.div
variants={front}
style={{ transformOrigin: "bottom" }}
className="absolute inset-x-0 bottom-0 z-10 h-24 rounded-xl border border-border/60 bg-linear-to-b from-foreground/30 to-foreground/20 backdrop-blur-sm"
/>
</motion.div>
)
}