Cards
FilingTab
Manila folders in a drawer with staggered typed tabs. Press a tab and the folders in front drop down to show its sheet, and a rubber stamp lands on it. Every tab stays readable.
Download
- React
- Tailwind CSS v4
- motion
components/ui/filing-tab.tsx
"use client";
import { useState, type ReactNode } from "react";
import { motion, useReducedMotion } from "motion/react";
type Folder = {
/** Typed on the tab. */
tab: string;
title: string;
/** What the sheet says. */
body: ReactNode;
/** Rubber stamp that lands when the folder opens. */
stamp?: string;
/** Folder colour. */
color?: string;
};
type FilingTabProps = {
folders: Folder[];
/** Index of the folder open at the start. */
open?: number;
className?: string;
};
const MANILA = ["#e6cf9b", "#e8b49a", "#b9cfc6", "#d8c6a0", "#c3c8de", "#e4d27c"];
const TAB = 1.9;
const HEIGHT = 22.2;
const BACK = 0.8;
const FRONT = 2.3;
/* FilingTab: manila folders in a drawer with staggered typed tabs. Press a tab and the folders in front drop down to show its sheet. */
export function FilingTab({ folders, open = 0, className = "" }: FilingTabProps) {
const reduce = useReducedMotion();
const [picked, setPicked] = useState(open);
const [near, setNear] = useState(-1);
const count = folders.length;
const tabWidth = Math.min(11, 31.3 / count);
const spring = reduce ? { duration: 0 } : { type: "spring" as const, stiffness: 190, damping: 21 };
return (
<div className={`@container w-full max-w-[560px] ${className}`}>
<div className="relative aspect-[3/2] overflow-hidden border-b-[0.15em] border-[#5b5140] text-[3cqw] leading-snug text-[#2b2a26]">
{folders.map((f, i) => {
const isOpen = i === picked;
const y = i <= picked ? i * BACK : HEIGHT - (count - i) * FRONT;
const color = f.color ?? MANILA[i % MANILA.length];
return (
<motion.div
key={f.tab}
className="pointer-events-none absolute inset-0"
style={{ zIndex: i }}
initial={false}
animate={{ y: `${y - (near === i && !isOpen ? 0.45 : 0)}em` }}
transition={spring}
>
<button
type="button"
aria-expanded={isOpen}
onClick={() => setPicked(i)}
onPointerEnter={() => setNear(i)}
onPointerLeave={() => setNear(-1)}
style={{ left: `${1 + i * tabWidth}em`, width: `${tabWidth - 0.2}em`, height: `${TAB + 0.1}em`, background: color }}
className="pointer-events-auto absolute top-0 truncate rounded-t-[0.5em] px-[0.5em] pb-[0.15em] text-center font-mono text-[0.98em] font-bold uppercase tracking-wide shadow-[inset_0_0.08em_0_rgba(255,255,255,0.5)]"
>
{f.tab}
</button>
<div
style={{ top: `${TAB}em`, background: color }}
inert={!isOpen}
className="pointer-events-auto absolute inset-x-0 bottom-0 rounded-t-[0.3em] shadow-[0_-0.1em_0.45em_rgba(50,35,10,0.42),inset_0_0.08em_0_rgba(255,255,255,0.45)]"
>
<motion.div
className="absolute inset-x-[1em] top-[0.8em] h-[13.5em] overflow-hidden bg-[#fbf8ef] bg-[repeating-linear-gradient(transparent_0_1.32em,rgba(40,70,130,0.13)_1.32em_1.38em)] px-[1em] pt-[0.7em] font-mono shadow-[0_0.1em_0.25em_rgba(0,0,0,0.3)]"
initial={false}
animate={{ y: isOpen ? 0 : "1.2em", rotate: isOpen ? -0.6 : 0.8 }}
transition={spring}
>
<p className="pr-[3.5em] text-[1.06em] font-bold">{f.title}</p>
<div className="mt-[0.3em] text-[0.82em] leading-[1.65]">{f.body}</div>
{f.stamp && isOpen && (
<motion.span
aria-hidden
className="absolute right-[0.7em] top-[0.5em] border-[0.14em] border-[#b3261e] px-[0.4em] text-[0.72em] font-black uppercase tracking-widest text-[#b3261e] mix-blend-multiply"
initial={reduce ? false : { scale: 2.4, opacity: 0, rotate: 2 }}
animate={{ scale: 1, opacity: 0.85, rotate: 9 }}
transition={{ type: "spring", stiffness: 560, damping: 24, delay: 0.26 }}
>
{f.stamp}
</motion.span>
)}
</motion.div>
</div>
</motion.div>
);
})}
</div>
</div>
);
}
1. Install the dependency.
npm i motion2. Paste the Code tab into
components/ui/filing-tab.tsx. The styles are Tailwind CSS v4 classes.3. Use it. This is the preview above:
app/filing-tab-demo.tsx
import { FilingTab } from "@/components/ui/filing-tab"; export default function FilingTabExample() { return ( <div className="grid h-full place-items-center overflow-hidden bg-[#2d3a35] bg-[radial-gradient(circle_at_50%_0%,#46584f,transparent_70%)] p-4"> <FilingTab folders={[ { tab: "Plans", title: "Plans and pricing", body: "Free forever for one project. Pro at ₹499 a month.", stamp: "Approved" }, { tab: "Team", title: "The team", body: "Six people across Chennai, Madurai and one very patient cat." }, { tab: "Press", title: "Press kit", body: "Logos, photos and a short story of the company." }, { tab: "Legal", title: "Legal", body: "Terms, privacy and refund rules in plain words.", stamp: "Filed" }, ]} /> </div> ); }
Props
| Name | Type | Default | What it does |
|---|---|---|---|
| folders | { tab, title, body, stamp?, color? }[] | — | One entry per folder, 2 to 6. Tab names must differ. |
| open | number | 0 | Index of the folder open at the start. |
| className | string | "" | Size. Everything inside scales with the width. |