File Manager Breadcrumb
Original · freefile path breadcrumb with toolbar
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/filemgr-breadcrumb.json"use client";
import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
type Crumb = {
id: string;
name: string;
itemCount: number;
};
type ViewMode = "grid" | "list";
type SortKey = "name" | "modified" | "size";
const FULL_PATH: Crumb[] = [
{ id: "root", name: "Workspace", itemCount: 14 },
{ id: "clients", name: "Clients", itemCount: 9 },
{ id: "northwind", name: "Northwind Rail", itemCount: 6 },
{ id: "2026", name: "2026", itemCount: 4 },
{ id: "q3-brand-refresh", name: "Q3 Brand Refresh", itemCount: 12 },
{ id: "exports", name: "Exports", itemCount: 5 },
{ id: "final-masters", name: "Final Masters", itemCount: 3 },
];
const SIBLINGS: Record<string, string[]> = {
root: ["Clients", "Archive", "Templates", "Shared with me"],
clients: ["Northwind Rail", "Halcyon Foods", "Meridian Health"],
northwind: ["2026", "2025", "Contracts"],
"2026": ["Q3 Brand Refresh", "Q2 Site Rebuild", "Q1 Discovery"],
"q3-brand-refresh": ["Exports", "Source Files", "Feedback"],
exports: ["Final Masters", "Web Optimised", "Print Ready"],
"final-masters": ["northwind-logo-master.ai", "typeface-specimen.pdf", "colour-tokens.json"],
};
const COLLAPSE_AT = 4;
function ChevronRight() {
return (
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-3.5 w-3.5">
<path
d="M9 5l7 7-7 7"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
function FolderIcon({ open }: { open: boolean }) {
return (
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-4 w-4 shrink-0">
{open ? (
<path
d="M3 7.5A1.5 1.5 0 014.5 6h4.2l1.8 2h6A1.5 1.5 0 0118 9.5v.5H7.6a2 2 0 00-1.94 1.515L4 18V7.5z"
stroke="currentColor"
strokeWidth="1.6"
strokeLinejoin="round"
/>
) : (
<path
d="M3 7.5A1.5 1.5 0 014.5 6h4.2l1.8 2h8A1.5 1.5 0 0120 9.5v8a1.5 1.5 0 01-1.5 1.5h-14A1.5 1.5 0 013 17.5v-10z"
stroke="currentColor"
strokeWidth="1.6"
strokeLinejoin="round"
/>
)}
{open ? (
<path
d="M4 18l1.66-6.485A2 2 0 017.6 10H20.4a1 1 0 01.97 1.243L19.9 17.5A2 2 0 0117.96 19H4z"
stroke="currentColor"
strokeWidth="1.6"
strokeLinejoin="round"
/>
) : null}
</svg>
);
}
function EllipsisIcon() {
return (
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" className="h-4 w-4">
<circle cx="5.5" cy="12" r="1.6" />
<circle cx="12" cy="12" r="1.6" />
<circle cx="18.5" cy="12" r="1.6" />
</svg>
);
}
function ArrowIcon({ dir }: { dir: "left" | "right" | "up" }) {
const d =
dir === "left"
? "M15 5l-7 7 7 7"
: dir === "right"
? "M9 5l7 7-7 7"
: "M12 19V5m0 0l-6 6m6-6l6 6";
return (
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-4 w-4">
<path d={d} stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
function GridIcon() {
return (
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-4 w-4">
<rect x="4" y="4" width="6.5" height="6.5" rx="1.5" stroke="currentColor" strokeWidth="1.7" />
<rect x="13.5" y="4" width="6.5" height="6.5" rx="1.5" stroke="currentColor" strokeWidth="1.7" />
<rect x="4" y="13.5" width="6.5" height="6.5" rx="1.5" stroke="currentColor" strokeWidth="1.7" />
<rect x="13.5" y="13.5" width="6.5" height="6.5" rx="1.5" stroke="currentColor" strokeWidth="1.7" />
</svg>
);
}
function ListIcon() {
return (
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-4 w-4">
<path
d="M4.5 6.5h15M4.5 12h15M4.5 17.5h15"
stroke="currentColor"
strokeWidth="1.7"
strokeLinecap="round"
/>
</svg>
);
}
function SortIcon() {
return (
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-4 w-4">
<path
d="M7 4v16m0 0l-3-3m3 3l3-3M17 20V4m0 0l-3 3m3-3l3 3"
stroke="currentColor"
strokeWidth="1.7"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
function CopyIcon({ done }: { done: boolean }) {
return (
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-4 w-4">
{done ? (
<path
d="M5 12.5l4.5 4.5L19 7"
stroke="currentColor"
strokeWidth="1.9"
strokeLinecap="round"
strokeLinejoin="round"
/>
) : (
<>
<rect x="9" y="9" width="11" height="11" rx="2" stroke="currentColor" strokeWidth="1.7" />
<path
d="M15 6.5A2.5 2.5 0 0012.5 4h-6A2.5 2.5 0 004 6.5v6A2.5 2.5 0 006.5 15"
stroke="currentColor"
strokeWidth="1.7"
strokeLinecap="round"
/>
</>
)}
</svg>
);
}
function UploadIcon() {
return (
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-4 w-4">
<path
d="M12 16V4m0 0L7.5 8.5M12 4l4.5 4.5M4 15v3a2 2 0 002 2h12a2 2 0 002-2v-3"
stroke="currentColor"
strokeWidth="1.7"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
export default function FilemgrBreadcrumb() {
const reduceMotion = useReducedMotion();
const uid = useId().replace(/[^a-zA-Z0-9]/g, "");
const [depth, setDepth] = useState<number>(FULL_PATH.length);
const [history, setHistory] = useState<number[]>([FULL_PATH.length]);
const [historyIndex, setHistoryIndex] = useState<number>(0);
const [openMenu, setOpenMenu] = useState<string | null>(null);
const [view, setView] = useState<ViewMode>("list");
const [sort, setSort] = useState<SortKey>("modified");
const [copied, setCopied] = useState<boolean>(false);
const [announcement, setAnnouncement] = useState<string>("");
const wrapRef = useRef<HTMLDivElement | null>(null);
const copyTimer = useRef<number | null>(null);
const trail = useMemo<Crumb[]>(() => FULL_PATH.slice(0, depth), [depth]);
const current = trail[trail.length - 1];
const pathString = useMemo(() => "/" + trail.map((c) => c.name).join("/"), [trail]);
const navigate = useCallback(
(nextDepth: number) => {
if (nextDepth === depth) return;
setHistory((prev) => {
const truncated = prev.slice(0, historyIndex + 1);
return [...truncated, nextDepth];
});
setHistoryIndex((i) => i + 1);
setDepth(nextDepth);
setOpenMenu(null);
setAnnouncement(`Opened ${FULL_PATH[nextDepth - 1].name}`);
},
[depth, historyIndex],
);
const goBack = useCallback(() => {
if (historyIndex <= 0) return;
const next = historyIndex - 1;
setHistoryIndex(next);
setDepth(history[next]);
setOpenMenu(null);
setAnnouncement(`Back to ${FULL_PATH[history[next] - 1].name}`);
}, [history, historyIndex]);
const goForward = useCallback(() => {
if (historyIndex >= history.length - 1) return;
const next = historyIndex + 1;
setHistoryIndex(next);
setDepth(history[next]);
setOpenMenu(null);
setAnnouncement(`Forward to ${FULL_PATH[history[next] - 1].name}`);
}, [history, historyIndex]);
const goUp = useCallback(() => {
if (depth <= 1) return;
navigate(depth - 1);
}, [depth, navigate]);
const copyPath = useCallback(() => {
const write = navigator.clipboard?.writeText(pathString);
const finish = () => {
setCopied(true);
setAnnouncement(`Path copied: ${pathString}`);
if (copyTimer.current !== null) window.clearTimeout(copyTimer.current);
copyTimer.current = window.setTimeout(() => setCopied(false), 1800);
};
if (write) {
write.then(finish).catch(finish);
} else {
finish();
}
}, [pathString]);
useEffect(() => {
return () => {
if (copyTimer.current !== null) window.clearTimeout(copyTimer.current);
};
}, []);
useEffect(() => {
if (!openMenu) return;
const onDown = (e: MouseEvent) => {
if (wrapRef.current && !wrapRef.current.contains(e.target as Node)) setOpenMenu(null);
};
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") setOpenMenu(null);
};
document.addEventListener("mousedown", onDown);
document.addEventListener("keydown", onKey);
return () => {
document.removeEventListener("mousedown", onDown);
document.removeEventListener("keydown", onKey);
};
}, [openMenu]);
const collapsed = trail.length > COLLAPSE_AT;
const head = collapsed ? trail.slice(0, 1) : trail;
const hidden = collapsed ? trail.slice(1, trail.length - 2) : [];
const tail = collapsed ? trail.slice(trail.length - 2) : [];
const fade = reduceMotion
? { initial: false as const, animate: {}, exit: {} }
: {
initial: { opacity: 0, y: -4, scale: 0.98 },
animate: { opacity: 1, y: 0, scale: 1 },
exit: { opacity: 0, y: -4, scale: 0.98 },
};
const btn =
"inline-flex items-center justify-center gap-1.5 rounded-lg border border-slate-200 bg-white px-2.5 py-1.5 text-sm font-medium text-slate-700 transition-colors hover:bg-slate-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-40 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-200 dark:hover:bg-zinc-800 dark:focus-visible:ring-indigo-400 dark:focus-visible:ring-offset-zinc-950";
const crumbBtn =
"inline-flex max-w-[10rem] items-center gap-1.5 truncate rounded-md px-2 py-1 text-sm text-slate-600 transition-colors hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1 focus-visible:ring-offset-white dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-50 dark:focus-visible:ring-indigo-400 dark:focus-visible:ring-offset-zinc-950";
const menuItem =
"flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-left text-sm text-slate-700 transition-colors hover:bg-slate-100 focus-visible:outline-none focus-visible:bg-slate-100 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-indigo-500 dark:text-zinc-300 dark:hover:bg-zinc-800 dark:focus-visible:bg-zinc-800 dark:focus-visible:ring-indigo-400";
const siblingsOf = (crumbId: string, index: number): string[] => {
const parentId = index === 0 ? null : FULL_PATH[index - 1].id;
return parentId ? (SIBLINGS[parentId] ?? []) : [];
};
const renderCrumb = (crumb: Crumb, absoluteIndex: number, isLast: boolean) => {
const menuId = `${uid}-menu-${crumb.id}`;
const opts = siblingsOf(crumb.id, absoluteIndex);
const isOpen = openMenu === crumb.id;
return (
<li key={crumb.id} className="flex shrink-0 items-center">
<div className="relative flex items-center">
{isLast ? (
<span
aria-current="page"
className="inline-flex max-w-[12rem] items-center gap-1.5 truncate rounded-md bg-indigo-50 px-2 py-1 text-sm font-semibold text-indigo-700 ring-1 ring-inset ring-indigo-200 dark:bg-indigo-500/10 dark:text-indigo-300 dark:ring-indigo-400/30"
>
<FolderIcon open />
<span className="truncate">{crumb.name}</span>
</span>
) : (
<button
type="button"
onClick={() => navigate(absoluteIndex + 1)}
className={crumbBtn}
title={`Open ${crumb.name}`}
>
<FolderIcon open={false} />
<span className="truncate">{crumb.name}</span>
</button>
)}
{opts.length > 0 && !isLast ? (
<>
<button
type="button"
aria-label={`Sibling folders of ${crumb.name}`}
aria-haspopup="menu"
aria-expanded={isOpen}
aria-controls={isOpen ? menuId : undefined}
onClick={() => setOpenMenu(isOpen ? null : crumb.id)}
className="ml-0.5 inline-flex h-6 w-5 items-center justify-center rounded text-slate-400 transition-colors hover:bg-slate-100 hover:text-slate-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:text-zinc-500 dark:hover:bg-zinc-800 dark:hover:text-zinc-200 dark:focus-visible:ring-indigo-400"
>
<ChevronRight />
</button>
<AnimatePresence>
{isOpen ? (
<motion.div
id={menuId}
role="menu"
aria-label={`Folders beside ${crumb.name}`}
{...fade}
transition={{ duration: 0.14, ease: "easeOut" }}
className="absolute left-0 top-full z-30 mt-1.5 w-52 rounded-xl border border-slate-200 bg-white p-1 shadow-xl shadow-slate-900/10 dark:border-zinc-700 dark:bg-zinc-900 dark:shadow-black/40"
>
{opts.map((name) => {
const isCurrentBranch = FULL_PATH[absoluteIndex]?.name === name;
return (
<button
key={name}
type="button"
role="menuitem"
onClick={() => {
if (isCurrentBranch) {
navigate(absoluteIndex + 2);
} else {
setOpenMenu(null);
setAnnouncement(`${name} is empty in this demo path`);
}
}}
className={menuItem}
>
<span className="text-slate-400 dark:text-zinc-500">
<FolderIcon open={false} />
</span>
<span className="truncate">{name}</span>
{isCurrentBranch ? (
<span className="ml-auto text-[10px] font-semibold uppercase tracking-wide text-indigo-600 dark:text-indigo-400">
open
</span>
) : null}
</button>
);
})}
</motion.div>
) : null}
</AnimatePresence>
</>
) : (
!isLast && (
<span className="mx-0.5 text-slate-300 dark:text-zinc-600">
<ChevronRight />
</span>
)
)}
</div>
</li>
);
};
return (
<section className="relative w-full bg-slate-50 px-4 py-16 sm:px-6 sm:py-24 dark:bg-zinc-950">
<style>{`
@keyframes ${uid}-fmbc-sweep {
0% { transform: translateX(-100%); }
100% { transform: translateX(220%); }
}
.${uid}-fmbc-sweep::after {
content: "";
position: absolute;
inset: 0;
width: 40%;
background: linear-gradient(90deg, transparent, rgba(99,102,241,0.16), transparent);
animation: ${uid}-fmbc-sweep 2.6s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
.${uid}-fmbc-sweep::after { animation: none; opacity: 0; }
}
`}</style>
<div className="mx-auto w-full max-w-5xl">
<header className="mb-6">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-indigo-600 dark:text-indigo-400">
File manager
</p>
<h2 className="mt-2 text-2xl font-bold tracking-tight text-slate-900 sm:text-3xl dark:text-zinc-50">
Path breadcrumb with toolbar
</h2>
<p className="mt-2 max-w-2xl text-sm text-slate-600 dark:text-zinc-400">
Long paths collapse behind an overflow menu. Every crumb opens its folder, and the chevron
beside it lists sibling folders at that level.
</p>
</header>
<div
ref={wrapRef}
className="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-zinc-800 dark:bg-zinc-900"
>
<div className="relative h-0.5 overflow-hidden bg-slate-100 dark:bg-zinc-800">
<div className={`${uid}-fmbc-sweep absolute inset-0`} aria-hidden="true" />
</div>
<div className="flex flex-wrap items-center gap-2 border-b border-slate-200 px-3 py-2.5 dark:border-zinc-800">
<div className="flex items-center gap-1" role="group" aria-label="History navigation">
<button
type="button"
onClick={goBack}
disabled={historyIndex <= 0}
aria-label="Go back"
title="Back"
className={`${btn} px-2`}
>
<ArrowIcon dir="left" />
</button>
<button
type="button"
onClick={goForward}
disabled={historyIndex >= history.length - 1}
aria-label="Go forward"
title="Forward"
className={`${btn} px-2`}
>
<ArrowIcon dir="right" />
</button>
<button
type="button"
onClick={goUp}
disabled={depth <= 1}
aria-label="Go up one level"
title="Up one level"
className={`${btn} px-2`}
>
<ArrowIcon dir="up" />
</button>
</div>
<div className="mx-1 hidden h-6 w-px bg-slate-200 sm:block dark:bg-zinc-800" />
<nav aria-label="Folder path" className="min-w-0 flex-1">
<ol className="flex flex-nowrap items-center gap-0.5 overflow-x-auto py-0.5">
{head.map((crumb, i) => renderCrumb(crumb, i, trail.length === 1 && i === 0))}
{collapsed ? (
<li className="flex shrink-0 items-center">
<span className="mx-0.5 text-slate-300 dark:text-zinc-600">
<ChevronRight />
</span>
<div className="relative">
<button
type="button"
aria-label={`Show ${hidden.length} hidden folders in path`}
aria-haspopup="menu"
aria-expanded={openMenu === "__overflow"}
aria-controls={openMenu === "__overflow" ? `${uid}-overflow` : undefined}
onClick={() => setOpenMenu(openMenu === "__overflow" ? null : "__overflow")}
className="inline-flex h-7 w-8 items-center justify-center rounded-md text-slate-500 transition-colors hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1 focus-visible:ring-offset-white dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-50 dark:focus-visible:ring-indigo-400 dark:focus-visible:ring-offset-zinc-950"
>
<EllipsisIcon />
</button>
<AnimatePresence>
{openMenu === "__overflow" ? (
<motion.div
id={`${uid}-overflow`}
role="menu"
aria-label="Hidden path segments"
{...fade}
transition={{ duration: 0.14, ease: "easeOut" }}
className="absolute left-0 top-full z-30 mt-1.5 w-56 rounded-xl border border-slate-200 bg-white p-1 shadow-xl shadow-slate-900/10 dark:border-zinc-700 dark:bg-zinc-900 dark:shadow-black/40"
>
{hidden.map((crumb) => {
const idx = FULL_PATH.findIndex((c) => c.id === crumb.id);
return (
<button
key={crumb.id}
type="button"
role="menuitem"
onClick={() => navigate(idx + 1)}
className={menuItem}
>
<span className="text-slate-400 dark:text-zinc-500">
<FolderIcon open={false} />
</span>
<span className="truncate">{crumb.name}</span>
<span className="ml-auto shrink-0 text-xs tabular-nums text-slate-400 dark:text-zinc-500">
{crumb.itemCount}
</span>
</button>
);
})}
</motion.div>
) : null}
</AnimatePresence>
</div>
<span className="mx-0.5 text-slate-300 dark:text-zinc-600">
<ChevronRight />
</span>
</li>
) : null}
{tail.map((crumb, i) => {
const idx = FULL_PATH.findIndex((c) => c.id === crumb.id);
return renderCrumb(crumb, idx, i === tail.length - 1);
})}
</ol>
</nav>
<div className="ml-auto flex items-center gap-1.5">
<button
type="button"
onClick={copyPath}
aria-label={copied ? "Path copied to clipboard" : "Copy path to clipboard"}
title="Copy path"
className={`${btn} px-2 ${copied ? "text-emerald-600 dark:text-emerald-400" : ""}`}
>
<CopyIcon done={copied} />
</button>
<div
role="group"
aria-label="View mode"
className="flex items-center gap-0.5 rounded-lg border border-slate-200 bg-slate-50 p-0.5 dark:border-zinc-700 dark:bg-zinc-800/60"
>
{(["list", "grid"] as const).map((mode) => (
<button
key={mode}
type="button"
aria-pressed={view === mode}
aria-label={`${mode === "list" ? "List" : "Grid"} view`}
title={`${mode === "list" ? "List" : "Grid"} view`}
onClick={() => {
setView(mode);
setAnnouncement(`${mode === "list" ? "List" : "Grid"} view`);
}}
className={`inline-flex h-7 w-8 items-center justify-center rounded-md transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:focus-visible:ring-indigo-400 ${
view === mode
? "bg-white text-slate-900 shadow-sm dark:bg-zinc-950 dark:text-zinc-50"
: "text-slate-500 hover:text-slate-800 dark:text-zinc-400 dark:hover:text-zinc-100"
}`}
>
{mode === "list" ? <ListIcon /> : <GridIcon />}
</button>
))}
</div>
<label className="sr-only" htmlFor={`${uid}-sort`}>
Sort files by
</label>
<div className="relative">
<span className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 text-slate-400 dark:text-zinc-500">
<SortIcon />
</span>
<select
id={`${uid}-sort`}
value={sort}
onChange={(e) => {
const next = e.target.value as SortKey;
setSort(next);
setAnnouncement(`Sorted by ${next}`);
}}
className="appearance-none rounded-lg border border-slate-200 bg-white py-1.5 pl-8 pr-7 text-sm font-medium text-slate-700 transition-colors hover:bg-slate-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-200 dark:hover:bg-zinc-800 dark:focus-visible:ring-indigo-400 dark:focus-visible:ring-offset-zinc-950"
>
<option value="name">Name</option>
<option value="modified">Modified</option>
<option value="size">Size</option>
</select>
<span className="pointer-events-none absolute right-2 top-1/2 -translate-y-1/2 rotate-90 text-slate-400 dark:text-zinc-500">
<ChevronRight />
</span>
</div>
<button
type="button"
onClick={() => setAnnouncement(`Upload dialog would open in ${current.name}`)}
className="inline-flex items-center gap-1.5 rounded-lg bg-indigo-600 px-3 py-1.5 text-sm font-semibold text-white transition-colors hover:bg-indigo-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-indigo-400 dark:focus-visible:ring-offset-zinc-950"
>
<UploadIcon />
<span className="hidden sm:inline">Upload</span>
</button>
</div>
</div>
<div className="flex flex-wrap items-center justify-between gap-2 bg-slate-50/70 px-4 py-2 text-xs dark:bg-zinc-950/40">
<code className="truncate font-mono text-slate-500 dark:text-zinc-500">{pathString}</code>
<span className="shrink-0 text-slate-500 dark:text-zinc-500">
{current.itemCount} items · sorted by {sort} · {view} view
</span>
</div>
<div className="px-4 py-5">
<ul
className={
view === "grid"
? "grid grid-cols-2 gap-3 sm:grid-cols-3"
: "divide-y divide-slate-100 dark:divide-zinc-800"
}
>
{(SIBLINGS[current.id] ?? ["This folder is empty"]).map((name) => (
<li
key={name}
className={
view === "grid"
? "flex flex-col items-start gap-2 rounded-xl border border-slate-200 bg-slate-50/60 p-3 dark:border-zinc-800 dark:bg-zinc-800/30"
: "flex items-center gap-2.5 py-2.5"
}
>
<span className="text-slate-400 dark:text-zinc-500">
<FolderIcon open={false} />
</span>
<span className="truncate text-sm text-slate-700 dark:text-zinc-300">{name}</span>
</li>
))}
</ul>
</div>
</div>
<p aria-live="polite" className="sr-only">
{announcement}
</p>
</div>
</section>
);
}Dependencies
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
File Manager List
Originalfile manager list view

File Manager Grid
Originalfile manager grid view

File Manager Upload
Originalupload dropzone with progress

File Manager Tree
Originalfolder tree sidebar

File Manager Preview
Originalfile preview panel

File Manager Storage
Originalstorage usage widget

File Manager Recent
Originalrecent files list

Spotlight Hero
OriginalA centred hero with a soft radial spotlight, badge and dual call-to-action.

Split Hero
OriginalA two-column hero pairing a headline and CTAs with a product mock and social proof.

Gradient Spotlight Hero
OriginalA minimal centred hero with a soft gradient-mesh backdrop, announcement pill and dual call-to-action buttons.

App Preview Hero
OriginalA centred hero that pairs headline copy with a realistic product dashboard mock built entirely from markup, complete with browser chrome and a floating notification card.

Waitlist Capture Hero
OriginalA dark, focused hero with an inline email capture form and avatar social proof, ready for pre-launch waitlists.

