@markoradak/folder-card
v0.1.1
Published
Animated folder-style card with FLIP spring expansion, 3D lid rotation, and render props API
Maintainers
Readme
@markoradak/folder-card
Animated folder-style cards for React. FLIP spring expansion, 3D lid rotation, SVG notch masks, and a render props API.
Install
npm install @markoradak/folder-card motionQuick start
import {
FolderCardGroup,
FolderCard,
FolderCardItem,
} from "@markoradak/folder-card";
import "@markoradak/folder-card/styles.css";
function App() {
return (
<FolderCardGroup>
<FolderCard
id="example"
renderLid={() => (
<div style={{ padding: 24 }}>
<h3>Project Alpha</h3>
<p>Click to expand</p>
</div>
)}
renderDetail={(close) => (
<div style={{ padding: 32 }}>
<h2>Project Alpha</h2>
<FolderCardItem>
<p>Stagger-animated content.</p>
</FolderCardItem>
<button onClick={close}>Close</button>
</div>
)}
/>
</FolderCardGroup>
);
}Features
- FLIP spring animation — Cards spring from grid position to centered dialog using Motion physics.
- 3D lid rotation — Perspective-correct rotation on any edge (bottom, top, left, right, or auto).
- SVG notch mask — Folder-style tab cutouts at any corner or edge center with configurable radii.
- Render props API — Full control over card face, detail content, and tab. No opinionated markup.
- Stagger animations —
FolderCardItemwraps content for automatic staggered fade-in. - CSS variable theming — Override
--fc-*variables for colors, radii, shadows, and transitions.
Components
<FolderCardGroup>
Wraps all cards and manages shared state (which card is open, spring config, etc).
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| springConfig | { stiffness?: number; damping?: number } | { stiffness: 320, damping: 42 } | Spring physics for position/size and lid rotation |
| dialogViewportPadding | number | 32 | Padding (px) between dialog and viewport edge |
| contentRevealDelay | number | 200 | Delay (ms) before dialog content fades in |
| openAngle | number | per-hinge default | Override the default open angle (deg) for lid rotation |
| backdropDuration | number | 0.25 | Duration (s) of backdrop fade |
| exitDuration | number | 0.15 | Duration (s) of content fade-out on close |
| fadeLid | boolean | false | Fade lid out instead of 3D rotation |
| dialogMinWidth | number | 1000 | Minimum target width (px) the dialog tries to reach. Content wider than this grows freely up to the viewport. |
| backdropClassName | string | — | Class name for the backdrop element |
| dialogClassName | string | — | Class name for the dialog element |
| onOpen | (id: string) => void | — | Called when a card opens |
| onClose | (id: string) => void | — | Called when a card closes |
<FolderCard>
An individual card with a lid face and expandable detail.
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| id | string | required | Unique identifier |
| renderLid | () => ReactNode | required | Card face / expanded lid content |
| renderDetail | (close: () => void) => ReactNode | required | Expanded dialog content. Rendered into two React trees (a hidden measurement tree and the visible dialog) — every component inside mounts twice. Keep side-effect-free; lift data fetching and subscriptions to a parent. |
| renderTab | () => ReactNode | — | Tab notch content |
| ariaLabel | string | — | Accessible label for the expanded dialog |
| className | string | — | Class name for the card button |
| style | CSSProperties | — | Inline styles for the card wrapper |
| perspective | number | 1800 | Perspective depth (px) for 3D rotation |
| hingeSide | 'auto' \| 'bottom' \| 'top' \| 'left' \| 'right' | 'auto' | Which edge the lid hinges on |
| notchPosition | NotchPosition | 'top-right' | Where the tab notch sits (8 positions) |
| notchOuterRadius | number | card radius | Outer concave radius of the notch cutout |
| notchInnerRadius | number | cardRadius * 0.7 | Inner inverted radius of the notch cutout |
| liveRadius | boolean | false | Observe --fc-radius changes reactively |
<FolderCardItem>
Wraps content inside renderLid or renderDetail for staggered fade-in animation.
CSS variables
Import the base stylesheet and override variables to theme:
@import "@markoradak/folder-card/styles.css";
:root {
--fc-radius: 20px;
--fc-card-bg: #ffffff;
--fc-foreground: #0a0a0a;
--fc-border: rgba(0, 0, 0, 0.1);
--fc-muted: rgba(0, 0, 0, 0.45);
--fc-shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.08);
--fc-shadow-lg: 0 8px 30px rgba(0, 0, 0, 0.12);
--fc-backdrop-bg: rgba(0, 0, 0, 0.5);
--fc-dialog-shadow: 0 25px 60px rgba(0, 0, 0, 0.25);
--fc-focus-ring: rgba(0, 0, 0, 0.2);
--fc-lid: color-mix(in srgb, var(--fc-foreground) 6%, var(--fc-card-bg));
--fc-lid-border: transparent; /* advanced: fallback only */
--fc-lid-back: /* gradient */; /* advanced: fallback only */
--fc-transition-duration: 300ms;
--fc-transition-easing: ease-out;
}Dark mode is applied automatically via @media (prefers-color-scheme: dark). To integrate with a class-based theme switcher (e.g. next-themes), override the variables inside your own .dark selector.
Development
This repository is a pnpm + Turborepo monorepo:
packages/
react/ @markoradak/folder-card — the published package
apps/
web/ Next.js demo + documentation siteRequires Node 18+ and pnpm 10+.
pnpm install
pnpm dev # run the demo site + package in watch mode
pnpm build # build the package
pnpm typecheck # typecheck all workspaces
cd packages/react
pnpm test # run the package test suiteLicense
MIT
