A file manager gives users a familiar way to organize files and folders. Here is how to build one.
Step 1: Types
// types/files.ts
export interface FileItem {
id: string;
name: string;
type: "file" | "folder";
mimeType?: string;
size?: number;
parentId: string | null;
createdAt: string;
updatedAt: string;
}
Step 2: File Manager State
"use client";
import { useState, useMemo } from "react";
import type { FileItem } from "@/types/files";
export function useFileManager(initialFiles: FileItem[]) {
const [files, setFiles] = useState(initialFiles);
const [currentFolderId, setCurrentFolderId] = useState<string | null>(null);
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
const [viewMode, setViewMode] = useState<"grid" | "list">("grid");
const currentFiles = useMemo(
() => files.filter((f) => f.parentId === currentFolderId),
[files, currentFolderId]
);
const breadcrumbs = useMemo(() => {
const path: FileItem[] = [];
let folderId = currentFolderId;
while (folderId) {
const folder = files.find((f) => f.id === folderId);
if (folder) {
path.unshift(folder);
folderId = folder.parentId;
} else break;
}
return path;
}, [files, currentFolderId]);
function navigateTo(folderId: string | null) {
setCurrentFolderId(folderId);
setSelectedIds(new Set());
}
function toggleSelect(id: string, multi = false) {
setSelectedIds((prev) => {
const next = multi ? new Set(prev) : new Set<string>();
if (next.has(id)) next.delete(id);
else next.add(id);
return next;
});
}
function createFolder(name: string) {
const newFolder: FileItem = {
id: crypto.randomUUID(),
name,
type: "folder",
parentId: currentFolderId,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
setFiles((prev) => [...prev, newFolder]);
}
function deleteSelected() {
setFiles((prev) => prev.filter((f) => !selectedIds.has(f.id)));
setSelectedIds(new Set());
}
function renameFile(id: string, newName: string) {
setFiles((prev) =>
prev.map((f) =>
f.id === id ? { ...f, name: newName, updatedAt: new Date().toISOString() } : f
)
);
}
return {
currentFiles,
breadcrumbs,
currentFolderId,
selectedIds,
viewMode,
setViewMode,
navigateTo,
toggleSelect,
createFolder,
deleteSelected,
renameFile,
};
}
Step 3: File Manager Component
"use client";
import { Folder, File, Grid, List, Plus, Trash, ChevronRight } from "lucide-react";
import { useFileManager } from "./useFileManager";
import type { FileItem } from "@/types/files";
export function FileManager({ files }: { files: FileItem[] }) {
const {
currentFiles,
breadcrumbs,
selectedIds,
viewMode,
setViewMode,
navigateTo,
toggleSelect,
createFolder,
deleteSelected,
} = useFileManager(files);
return (
<div className="rounded-xl border dark:border-gray-700">
{/* Toolbar */}
<div className="flex items-center justify-between border-b p-3 dark:border-gray-700">
<div className="flex items-center gap-2">
<button
onClick={() => {
const name = prompt("Folder name:");
if (name) createFolder(name);
}}
className="flex items-center gap-1 rounded-lg bg-blue-600 px-3 py-1.5 text-sm text-white hover:bg-blue-700"
>
<Plus className="h-4 w-4" /> New Folder
</button>
{selectedIds.size > 0 && (
<button
onClick={deleteSelected}
className="flex items-center gap-1 rounded-lg bg-red-600 px-3 py-1.5 text-sm text-white hover:bg-red-700"
>
<Trash className="h-4 w-4" /> Delete ({selectedIds.size})
</button>
)}
</div>
<div className="flex gap-1">
<button
onClick={() => setViewMode("grid")}
className={`rounded-lg p-1.5 ${viewMode === "grid" ? "bg-gray-200 dark:bg-gray-700" : ""}`}
>
<Grid className="h-4 w-4" />
</button>
<button
onClick={() => setViewMode("list")}
className={`rounded-lg p-1.5 ${viewMode === "list" ? "bg-gray-200 dark:bg-gray-700" : ""}`}
>
<List className="h-4 w-4" />
</button>
</div>
</div>
{/* Breadcrumbs */}
<div className="flex items-center gap-1 border-b px-3 py-2 text-sm dark:border-gray-700">
<button
onClick={() => navigateTo(null)}
className="text-blue-600 hover:underline"
>
Home
</button>
{breadcrumbs.map((crumb) => (
<span key={crumb.id} className="flex items-center gap-1">
<ChevronRight className="h-3 w-3 text-gray-400" />
<button
onClick={() => navigateTo(crumb.id)}
className="text-blue-600 hover:underline"
>
{crumb.name}
</button>
</span>
))}
</div>
{/* File listing */}
<div className={viewMode === "grid"
? "grid grid-cols-2 gap-2 p-3 sm:grid-cols-4 md:grid-cols-6"
: "divide-y p-1 dark:divide-gray-800"
}>
{currentFiles.length === 0 ? (
<p className="col-span-full p-8 text-center text-sm text-gray-500">
This folder is empty
</p>
) : (
currentFiles.map((file) =>
viewMode === "grid" ? (
<GridItem
key={file.id}
file={file}
selected={selectedIds.has(file.id)}
onSelect={(multi) => toggleSelect(file.id, multi)}
onOpen={() => file.type === "folder" && navigateTo(file.id)}
/>
) : (
<ListItem
key={file.id}
file={file}
selected={selectedIds.has(file.id)}
onSelect={(multi) => toggleSelect(file.id, multi)}
onOpen={() => file.type === "folder" && navigateTo(file.id)}
/>
)
)
)}
</div>
</div>
);
}
function GridItem({
file,
selected,
onSelect,
onOpen,
}: {
file: FileItem;
selected: boolean;
onSelect: (multi: boolean) => void;
onOpen: () => void;
}) {
return (
<div
onClick={(e) => onSelect(e.metaKey || e.ctrlKey)}
onDoubleClick={onOpen}
className={`flex cursor-pointer flex-col items-center rounded-lg p-3 transition-colors hover:bg-gray-100 dark:hover:bg-gray-800 ${
selected ? "bg-blue-50 ring-2 ring-blue-500 dark:bg-blue-950" : ""
}`}
>
{file.type === "folder" ? (
<Folder className="h-10 w-10 text-blue-500" />
) : (
<File className="h-10 w-10 text-gray-400" />
)}
<span className="mt-1 max-w-full truncate text-xs">{file.name}</span>
</div>
);
}
function ListItem({
file,
selected,
onSelect,
onOpen,
}: {
file: FileItem;
selected: boolean;
onSelect: (multi: boolean) => void;
onOpen: () => void;
}) {
return (
<div
onClick={(e) => onSelect(e.metaKey || e.ctrlKey)}
onDoubleClick={onOpen}
className={`flex cursor-pointer items-center gap-3 px-3 py-2 transition-colors hover:bg-gray-100 dark:hover:bg-gray-800 ${
selected ? "bg-blue-50 dark:bg-blue-950" : ""
}`}
>
{file.type === "folder" ? (
<Folder className="h-5 w-5 text-blue-500" />
) : (
<File className="h-5 w-5 text-gray-400" />
)}
<span className="flex-1 text-sm">{file.name}</span>
{file.size && (
<span className="text-xs text-gray-400">
{(file.size / 1024).toFixed(1)} KB
</span>
)}
<span className="text-xs text-gray-400">
{new Date(file.updatedAt).toLocaleDateString()}
</span>
</div>
);
}
Need a File Management System?
We build web applications with file management, cloud storage integration, and document workflows. Contact us to discuss your project.