File manager React component for Tailwind CSS v4
v1.0.0Added in 1.0.0
A React file manager for the documents of a company or a record: folders and files in a grid or a list, a folder tree, uploads and bulk actions.
Click a file or a folder to select it, Cmd or Ctrl + click to add or remove one, Shift + click for a range, tick the checkbox that appears on hover, or click empty space to clear the selection; with anything selected the toolbar turns into the bulk bar (Move, Download, Delete, Select all). Double-click (or Enter) opens: a folder slides in, an image opens in the lightbox with the folder's other images, any other file goes to onOpen or its url in a new tab. Right-click or the ⋯ button opens the menu (Open, Rename, Move to, Download, your own entries, Delete); right-click on empty space offers New folder, Upload and Select all. Rename with F2, the menu or a slow second click on the name: the base name is selected, Enter saves, Escape cancels, and a rejected promise shows its message under the field. Drag files (the whole selection comes along) onto a folder, a crumb or a folder in the tree to move them; a folder never drops into itself, and the drag card shows a stop sign over a refused target. Drop files from the desktop anywhere on it to upload into the open folder, or onto a folder tile to upload there; pasting files uploads too, and the uploads panel in the bottom corner shows progress and rejected files, folds away and closes once nothing runs. Search looks through every folder, lists the results with their path and marks the matched letters. With the keyboard the files are one Tab stop: arrows move (Up and Down by a row of tiles), Home, End, Page Up and Page Down jump, letters jump to a name, Space toggles, Shift + arrows extend, Cmd or Ctrl + A selects all, Escape clears, Delete asks and removes, Backspace or Alt + Up goes to the parent folder, Shift + F10 opens the menu, and Cmd or Ctrl + Shift + N makes a folder from anywhere inside. On a phone it starts as a list under the folder's name as a large title with a back link: a tap opens, a long press starts selecting (checkboxes appear, taps add or remove, a floating bar offers Move, Download and Delete), hold and drag a selected file onto a folder to move it, and every menu, the Move dialog and the new folder dialog are bottom sheets. The layout follows the manager's own width, so in a narrow side panel it shows the compact list and two-column tiles even on a wide screen.
File manager is a Pro React component in the React data display components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add file-manager, together with Alert Dialog, Avatar, Breadcrumb, Button, Checkbox and 27 more and the npm packages @dnd-kit/core, motion and lucide-react.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add file-managerFile manager is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans
Usage
Import FileManager from @/components/ui/file-manager.
import { useState } from "react"
import { FileManager, type FileManagerItem, type FileManagerUpload } from "@/components/ui/file-manager"
export function Documents({ initial }: { initial: FileManagerItem[] }) {
const [items, setItems] = useState(initial)
const [uploads, setUploads] = useState<FileManagerUpload[]>([])
return (
<FileManager
items={items}
uploads={uploads}
quota={{ used: 2.4 * 1024 ** 3, total: 10 * 1024 ** 3 }}
locale="en-US"
persistKey="crm:documents"
labels={{ root: "Company files", search: "Search company files" }}
onUpload={async (files, folderId) => {
setUploads(files.map((file) => ({ id: file.name, name: file.name, size: file.size, progress: 0, status: "uploading", folderId })))
const body = new FormData()
files.forEach((file) => body.append("files", file))
const saved: FileManagerItem[] = await (await fetch(`/api/files?folder=${folderId ?? ""}`, { method: "POST", body })).json()
setItems((list) => [...list, ...saved])
setUploads((list) => list.map((upload) => ({ ...upload, progress: 1, status: "done" })))
}}
onCreateFolder={async (name, parentId) => {
const folder: FileManagerItem = await (await fetch("/api/folders", { method: "POST", body: JSON.stringify({ name, parentId }) })).json()
setItems((list) => [...list, folder])
}}
onRename={async (item, name) => {
await fetch(`/api/files/${item.id}`, { method: "PATCH", body: JSON.stringify({ name }) })
setItems((list) => list.map((i) => (i.id === item.id ? { ...i, name } : i)))
}}
onMove={async (ids, parentId) => {
await fetch("/api/files/move", { method: "POST", body: JSON.stringify({ ids, parentId }) })
setItems((list) => list.map((i) => (ids.includes(i.id) ? { ...i, parentId } : i)))
}}
onDelete={async (ids) => {
await fetch("/api/files", { method: "DELETE", body: JSON.stringify({ ids }) })
setItems((list) => list.filter((i) => !ids.includes(i.id)))
}}
/>
)
}Props
items*default"[]"FileManagerItem[]The whole tree, flat: { id, kind: "folder" | "file", name, parentId (null at the root), size? (bytes), type? (mime), url?, thumbnailUrl?, modifiedAt?, modifiedBy?: { name, avatar? }, itemCount?, shared?, starred?, locked? }. The component filters by parentId; a parentId loop never hangs. Locked items cannot be renamed, moved or deleted; Move and Delete on a selection that holds some skip them. Tiles use content-visibility past 60 items; page from the server beyond about 2.000 files in one folder.
rootLabeldefault""stringThe root's name in the breadcrumb, the folder tree and search paths. Empty uses labels.root ("All files").
quota{ used: number; total: number }Bytes used and available: a thin bar with "2,4 GB of 10 GB" next to the breadcrumb, amber past 80% and red past 95%.
uploadsdefault"[]"FileManagerUpload[]The uploads you are running: { id, name, size, progress (0 to 1), status: "uploading" | "done" | "error", error?, folderId }. They show in a floating panel at the bottom corner (collapsible, dismissible once nothing is running), next to the files the picker or a drop rejected.
emptyStateReactNodeReplaces the empty folder state (by default an icon, a line of text and New folder / Upload when those handlers exist).
labelsPartial<FileManagerLabels>Every string, English by default (DEFAULT_FILE_MANAGER_LABELS): root, region, folders, search, newFolder, folderName, create, cancel, upload, dropHere(folder), move, moveTo, moveHere, moveDescription(count, name), rename, download, delete, deleteTitle(count, name), deleteDescription, open, preview, select(name), selectAll, clearSelection, selected(count, formatted), items(count, formatted), results(count, formatted), name, modified, size, owner, type, sort, ascending, descending, view, grid, list, actions(name), sheetTitle, uploads, uploading(count, formatted), uploadsDone(count, formatted), uploadsFailed(count, formatted), uploadDone, uploadFailed, rejectedType, rejectedSize(max), collapse, expand, dismiss, storage, quota(used, total), empty, emptyDescription, noResults, noResultsDescription(query), clearSearch, loading, errorTitle, retry, moved(count, folder), renamed(name), deleted(count), created(name), failed, locked, shared, starred, folder, modifiedOn(when), dragging(count, name), pickedUp(count, name), dropped, dragCancelled, kinds (names per file kind).
defaultViewdefault"grid""grid" | "list"The first layout while uncontrolled: tiles with previews, or rows with Modified, Size and Owner columns. Phones start with the list until someone picks a view; search results are always a list.
view"grid" | "list"The layout, controlled. Pair it with onViewChange.
onViewChange(view: "grid" | "list") => voidCalled when the view switch in the toolbar changes.
tileSizedefault"md""sm" | "md" | "lg"The smallest tile width in the grid, 120 / 160 / 200px; the grid fits as many columns as it can. Below 512px of width (phones, side panels) it shows two columns.
variantdefault"surface""surface" | "plain"surface sits the manager in a raised panel; plain drops the panel, for a section card that already has one.
tonedefault"neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"Selected tiles and rows, drop targets, checkboxes, the tree's current folder, progress bars and the Upload button of the empty state.
colordefault""stringAny CSS color in place of the tone.
showTreedefaulttruebooleanThe folder tree beside the files once the manager itself is 768px wide (a container query, so it also hides in a narrow panel on a wide screen). Its folders are drop targets too.
showBreadcrumbdefaulttruebooleanThe path from the root to the open folder; every crumb is a drop target. Phones show a back link to the parent.
loadingdefaultfalsebooleanSkeleton tiles or rows with the real shape while the first page loads.
skeletonCountdefault12numberHow many skeletons show while loading.
errordefault""ReactNodeReplaces the files with an error state and its message; add onRetry for a Try again button.
onRetry() => (void | Promise<unknown>)The error state's Try again button.
defaultSearchdefault""stringA search to start with. Search looks through every folder, ignoring case and diacritics, and lists the results with their folder path.
searchstringThe search, controlled. Pair it with onSearchChange.
onSearchChange(search: string) => voidCalled with the debounced query, and with "" when a folder is opened from the results.
folderIdstring | nullThe open folder, controlled (null is the root). If it disappears from items, the nearest folder that still exists opens.
defaultFolderIddefault"null"string | nullThe folder that opens first while uncontrolled (null is the root). The playground offers folders of the demo.
onFolderChange(folderId: string | null) => voidCalled when a folder opens: a double-click, a crumb, the tree, Backspace.
sortFileManagerSortThe sort, controlled: { by: "name" | "modified" | "size" | "type", direction: "asc" | "desc" }. Folders always come first.
defaultSortdefault"DEFAULT_FILE_MANAGER_SORT"FileManagerSortThe first sort while uncontrolled: { by: "name", direction: "asc" }. Picking Modified or Size in the menu starts with the newest or largest.
onSortChange(sort: FileManagerSort) => voidCalled from the sort menu and from the column headers of the list.
selectedKeysstring[]The selected ids, controlled. Pair it with onSelectedKeysChange.
defaultSelectedKeysdefault"[]"string[]The ids selected at first while uncontrolled; with any selected the toolbar is the bulk bar.
onSelectedKeysChange(keys: string[]) => voidCalled when the selection changes. Opening another folder clears it.
persistKeydefault""stringRemembers the view and the sort in localStorage under this key ("<key>:view", "<key>:sort"). Empty remembers nothing.
confirmDeletedefaulttruebooleanAsk before deleting, in an alert dialog that names the file or the count. Off, Delete runs at once.
previewImagesdefaulttruebooleanImages open in the built-in lightbox with the folder's other images. Off, they go to onOpen like any other file.
acceptdefault""stringWhat the upload picker offers, like the input's accept ("image/*,.pdf"). Dropped or pasted files outside it are listed as rejected in the uploads panel instead of reaching onUpload.
maxFileSizedefault0numberThe largest file in bytes; bigger ones are listed as rejected with the limit. 0 means no limit.
uploadShortcutdefault""stringA key combo that opens the upload picker from anywhere on the page ("mod+u" notation). Empty binds nothing.
localedefault""stringFormats sizes ("2,4 MB"), dates, counts and the sort order. Empty uses the UIProvider locale, then the browser's.
nowDate | numberThe reference time for "modified 5 min ago". Defaults to a live clock that ticks every minute; demos pin 24.09.2026 16:40.
onOpen(item: FileManagerItem) => voidOpens a file. Folders always open in place and images in the lightbox (while previewImages is on). Without it a file with a url opens in a new tab.
onUpload(files: File[], folderId: string | null) => (void | Promise<unknown>)Turns on the Upload button, dropping files from the desktop and pasting them. folderId is the open folder, or the folder tile the files were dropped on. Report progress back through uploads.
onCreateFolder(name: string, parentId: string | null) => (void | Promise<unknown>)Turns on New folder (a dialog, a sheet on phones). A rejected promise keeps the dialog open with its message, so you decide about duplicate names.
onRename(item: FileManagerItem, name: string) => (void | Promise<unknown>)Turns on renaming in place. The field stays busy while the promise runs; a rejection shows its message under it and keeps it open.
onMove(ids: string[], targetFolderId: string | null) => (void | Promise<unknown>)Turns on dragging onto folders, crumbs and tree nodes, and Move to (a dialog with the folder tree, a sheet on phones). Moves into the current folder, into itself or into a subfolder are refused before it runs.
onDelete(ids: string[]) => (void | Promise<unknown>)Turns on Delete in the menus, the bulk bar and the Delete key. The confirmation stays loading while the promise runs.
onDownload(items: FileManagerItem[]) => voidDownload for files and selections. Without it Download appears when every file has a url and saves them through links.
getActions(item: FileManagerItem) => MenuEntry[]Extra menu entries per item (Share, Copy link, Star), between Download and Delete, in the W1 MenuEntry shape.
classNamesPartial<Record<"toolbar" | "breadcrumb" | "title" | "quota" | "body" | "tree" | "content" | "header" | "tile" | "row" | "select" | "icon" | "name" | "meta" | "actions" | "drop" | "uploads" | "empty" | "move" | "newFolder", string>>Extra classes for the parts. title is the phone's folder title, move and newFolder go on the dialogs' bodies, uploads on the floating panel.
classNamestringExtra classes for the root, merged last. Other props (id, aria-*, data-*, handlers) spread onto it.
Parts
buildFolderTree
functionThe folders as nested tree-view nodes with folder icons, sorted by name; a parentId loop is broken (a folder is never listed twice). Pure, so a server can call it. Signature: (items: FileManagerItem[], options?: { locale?: string }) => TreeNode[].
getFolderPath
functionThe trail from the root to a folder as breadcrumb entries (the root's id is "file-manager-root"). Pure and cycle-safe. Signature: (items: FileManagerItem[], folderId: string | null, options?: { rootLabel?: string }) => BreadcrumbEntry[].
DEFAULT_FILE_MANAGER_LABELS
constantThe English strings, to spread under a translation. Type: FileManagerLabels.
DEFAULT_FILE_MANAGER_SORT
constant{ by: "name", direction: "asc" }, the default sort. Type: FileManagerSort.
Examples
Search as a list
Invoices from every folder, largest first, each with its path.
A record's files
One client's documents: re-rooted, no tree, small tiles in brand orange.
Phone selection
Long press to select, then the floating bar for bulk actions.
Move dialog
Pick a destination in the folder tree; drops into the same folder are refused.
Loading, empty, uploading
Skeletons, an empty folder with its actions, and the uploads panel.
Read-only viewer
No handlers: browse, search, preview and open only.
Dependencies
Files
- components/ui/file-manager.tsx
FAQ
Is the React File manager component free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install File manager?
Run npx wingo-ui@latest add file-manager. It also adds the Wingo UI items it builds on (Alert Dialog, Avatar, Breadcrumb, Button, Checkbox and 27 more) and the npm packages @dnd-kit/core, motion and lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/file-manager.
How does File manager behave on phones?
On a phone it starts as a list under the folder's name as a large title with a back link: a tap opens, a long press starts selecting (checkboxes appear, taps add or remove, a floating bar offers Move, Download and Delete), hold and drag a selected file onto a folder to move it, and every menu, the Move dialog and the new folder dialog are bottom sheets.
See also
More React data display components
Articles using this component
- React File Manager: Folders, Uploads and the Backend API
Component guides10 min read
- React Tree View: Checkboxes, Lazy Loading, Drag to Move
Component guides10 min read