Interaction Choreography

Folder Filing Grid

A document filing React component: scattered document cards fly along an arc into their matching folders, which open to catch them, close with a small dip and roll their counts up. Auto-sort in one go, or file by hand with drag or keyboard, and open a folder to see what is inside.

Filing, turned into something you can watch. Document cards lie scattered on the desk above at deterministic angles and offsets (hashed from each id, so SSR and the client agree). With autoSort on, every interval milliseconds one document flies along an arc into the folder that matches its category — the folder opens as it approaches, closes with a small dip once it lands, and the badge in its corner rolls up by one. You can also drag documents onto folders yourself: the folder under the card opens and highlights, and in strict mode a wrong drop springs back home with a wiggle. Click a folder to open a small panel listing its documents; "Scatter again" sends everything back to the desk. The folders are drawn entirely with CSS gradients — no images.

Bring this into your project

Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.

npx webberui@latest add folder-filing-grid.en

New project or installation trouble? Start here →

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

350
Active edited usageDefault props
<FolderFilingGrid/>

The complete example includes the preview layout and data. Install the component first, then paste this file into your project.

Installation

npx webberui@latest add https://webberui.com/r/folder-filing-grid.en.json

Or, once registries are configured in components.json, install it as @webberui/folder-filing-grid.

Usage

import { FolderFilingGrid } from "@/components/ui/folder-filing-grid";

const documents = [
  { id: "d1", title: "March electricity bill", category: "invoice", meta: "$42.80" },
  { id: "d2", title: "Team retreat sign-up", category: "hr" },
  { id: "d3", title: "Q1 contract", category: "contract" },
];

const folders = [
  { id: "invoice", label: "Invoices" },
  { id: "hr", label: "People", color: "#38bdf8" },
  { id: "contract", label: "Contracts", color: "#a78bfa" },
];

<FolderFilingGrid
  documents={documents}
  folders={folders}
  autoSort
  onFiledChange={(filed) => console.log(filed)}
/>

Props

PropTypeDefaultDescription
documentsFilingDocument[]—Documents on the desk
foldersFilingFolder[]—The folder row below (3–5 recommended)
filedFiledMap—Controlled filing result; managed internally when omitted (uncontrolled)
defaultFiledFiledMap{}Initial filing result in uncontrolled mode
onFiledChange(filed: FiledMap) => void—Called when the filing result changes (each filed document, or a re-scatter)
autoSortbooleanfalseAuto-sort: while on, desk documents fly into their folders one by one
intervalnumber350Gap between documents during auto-sort (ms)
strictbooleantrueStrict mode: only the matching folder accepts a document and wrong drops bounce back; when off, any folder accepts it
draggablebooleantrueAllow dragging documents with mouse / touch
accentstring—Default folder colour (written to --wb-folder-filing-accent); a folder's own color wins
classNamestring—Passed through to the outermost container

FilingDocument

FieldTypeDefaultDescription
idstring—Unique id; also the key in the filed map
titlestring—Document title
categorystring—Category; matches a folder id, falling back to the last folder when nothing matches
metastring—Extra detail (amount, date…) shown under the title
iconReactNodeDocument iconCustom icon

FilingFolder

FieldTypeDefaultDescription
idstring—Unique id, matched against each document's category
labelstring—Folder name
colorstringvar(--wb-folder-filing-accent)Folder colour

FiledMap

Record<string, string>: document id → folder id. Entries pointing at a folder that does not exist are treated as unfiled.

How it works

  • Timing is exposed as CSS variables: --wb-folder-filing-fly (flight duration, default 560ms), --wb-folder-filing-open (folder opening, default 180ms) and --wb-folder-filing-accent (default folder colour, #f59e0b). The component re-reads them on every filing, so runtime overrides apply immediately.
  • Flight path: the component measures the card relative to the folder mouth, eases horizontally with [0.22, 1, 0.36, 1] and lifts 40px mid-flight for an arc, while rotation returns to zero, scale goes 1 → 0.4 and the card fades out at the end.
  • The folder's front panel opens with rotateX(-28deg) around its bottom edge (perspective: 600px), starting --wb-folder-filing-open before the document arrives so it catches it; after landing it closes and dips [0, 3, 0].
  • Drag hit testing compares the card centre with each folder's getBoundingClientRect; dropping on empty desk space springs the card back to its slot.
  • While autoSort is on the timer keeps running: after "Scatter again" the documents are sorted once more, which suits a showcase loop. To run it once, turn autoSort off when onFiledChange reports everything filed (see the demo).
  • No Math.random: scatter angles (±8°) and offsets are hashed from id and index, so the first render matches the server output.

Accessibility

  • Document cards are buttons with keyboard instructions via aria-describedby: Enter selects → arrow keys (or Home / End) move the target folder → Enter files, Esc cancels; the selected card carries aria-pressed. Clicking a document and then a folder also files it — no dragging required.
  • After filing with the keyboard, focus moves to the next document still on the desk instead of dropping back to the top of the page.
  • Each folder button's aria-label carries its name and count (e.g. "Invoices: 4 filed") and links to its document panel via aria-expanded / aria-controls; the panel closes on Esc or an outside click, and Esc returns focus to the folder.
  • Filing, rejection and scatter results are written to an aria-live="polite" region for screen readers.
  • When the system's reduce motion setting is on, documents simply disappear, folders neither open nor dip, counts update directly, and scattered documents snap straight back to their slots.