File Manager Tree
Original · freefolder tree sidebar
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-tree.json"use client";
import { useCallback, useMemo, useRef, useState, type KeyboardEvent } from "react";
import { motion, useReducedMotion } from "motion/react";
type NodeKind = "folder" | "file";
type FileLang =
| "tsx"
| "ts"
| "css"
| "json"
| "md"
| "svg"
| "sql"
| "img"
| "lock";
type TreeNode = {
id: string;
name: string;
kind: NodeKind;
lang?: FileLang;
size?: string;
changed?: "added" | "modified";
children?: TreeNode[];
};
const TREE: TreeNode[] = [
{
id: "app",
name: "app",
kind: "folder",
children: [
{
id: "app/(marketing)",
name: "(marketing)",
kind: "folder",
children: [
{
id: "app/(marketing)/pricing",
name: "pricing",
kind: "folder",
children: [
{
id: "app/(marketing)/pricing/page.tsx",
name: "page.tsx",
kind: "file",
lang: "tsx",
size: "8.4 KB",
changed: "modified",
},
{
id: "app/(marketing)/pricing/plans.ts",
name: "plans.ts",
kind: "file",
lang: "ts",
size: "2.1 KB",
},
],
},
{
id: "app/(marketing)/layout.tsx",
name: "layout.tsx",
kind: "file",
lang: "tsx",
size: "1.9 KB",
},
{
id: "app/(marketing)/page.tsx",
name: "page.tsx",
kind: "file",
lang: "tsx",
size: "12.7 KB",
},
],
},
{
id: "app/api",
name: "api",
kind: "folder",
children: [
{
id: "app/api/checkout",
name: "checkout",
kind: "folder",
children: [
{
id: "app/api/checkout/route.ts",
name: "route.ts",
kind: "file",
lang: "ts",
size: "3.3 KB",
changed: "added",
},
],
},
{
id: "app/api/webhooks",
name: "webhooks",
kind: "folder",
children: [
{
id: "app/api/webhooks/stripe.ts",
name: "stripe.ts",
kind: "file",
lang: "ts",
size: "5.8 KB",
},
],
},
],
},
{
id: "app/globals.css",
name: "globals.css",
kind: "file",
lang: "css",
size: "6.2 KB",
changed: "modified",
},
],
},
{
id: "components",
name: "components",
kind: "folder",
children: [
{
id: "components/ui",
name: "ui",
kind: "folder",
children: [
{
id: "components/ui/button.tsx",
name: "button.tsx",
kind: "file",
lang: "tsx",
size: "2.8 KB",
},
{
id: "components/ui/dialog.tsx",
name: "dialog.tsx",
kind: "file",
lang: "tsx",
size: "4.6 KB",
},
{
id: "components/ui/tooltip.tsx",
name: "tooltip.tsx",
kind: "file",
lang: "tsx",
size: "3.1 KB",
},
],
},
{
id: "components/nav-rail.tsx",
name: "nav-rail.tsx",
kind: "file",
lang: "tsx",
size: "7.0 KB",
},
],
},
{
id: "lib",
name: "lib",
kind: "folder",
children: [
{
id: "lib/db",
name: "db",
kind: "folder",
children: [
{
id: "lib/db/schema.sql",
name: "schema.sql",
kind: "file",
lang: "sql",
size: "9.4 KB",
},
{
id: "lib/db/client.ts",
name: "client.ts",
kind: "file",
lang: "ts",
size: "1.2 KB",
},
],
},
{
id: "lib/format-currency.ts",
name: "format-currency.ts",
kind: "file",
lang: "ts",
size: "0.9 KB",
},
],
},
{
id: "public",
name: "public",
kind: "folder",
children: [
{
id: "public/logo.svg",
name: "logo.svg",
kind: "file",
lang: "svg",
size: "4.1 KB",
},
{
id: "public/og-card.png",
name: "og-card.png",
kind: "file",
lang: "img",
size: "184 KB",
},
],
},
{
id: "README.md",
name: "README.md",
kind: "file",
lang: "md",
size: "3.7 KB",
},
{
id: "package.json",
name: "package.json",
kind: "file",
lang: "json",
size: "1.8 KB",
changed: "modified",
},
{
id: "pnpm-lock.yaml",
name: "pnpm-lock.yaml",
kind: "file",
lang: "lock",
size: "412 KB",
},
];
type FlatRow = {
node: TreeNode;
depth: number;
parentId: string | null;
setSize: number;
posInSet: number;
};
function flatten(
nodes: TreeNode[],
open: ReadonlySet<string>,
depth: number,
parentId: string | null,
out: FlatRow[]
): FlatRow[] {
nodes.forEach((node, index) => {
out.push({
node,
depth,
parentId,
setSize: nodes.length,
posInSet: index + 1,
});
if (node.kind === "folder" && node.children && open.has(node.id)) {
flatten(node.children, open, depth + 1, node.id, out);
}
});
return out;
}
function collectFolderIds(nodes: TreeNode[], out: string[]): string[] {
for (const node of nodes) {
if (node.kind === "folder") {
out.push(node.id);
if (node.children) collectFolderIds(node.children, out);
}
}
return out;
}
function matchTree(nodes: TreeNode[], query: string): TreeNode[] {
const q = query.trim().toLowerCase();
if (!q) return nodes;
const walk = (list: TreeNode[]): TreeNode[] => {
const kept: TreeNode[] = [];
for (const node of list) {
const selfHit = node.name.toLowerCase().includes(q);
const kids = node.children ? walk(node.children) : [];
if (selfHit || kids.length > 0) {
kept.push({
...node,
children: node.children ? (selfHit && kids.length === 0 ? node.children : kids) : undefined,
});
}
}
return kept;
};
return walk(nodes);
}
const LANG_TONE: Record<FileLang, string> = {
tsx: "text-sky-600 dark:text-sky-400",
ts: "text-indigo-600 dark:text-indigo-400",
css: "text-violet-600 dark:text-violet-400",
json: "text-amber-600 dark:text-amber-400",
md: "text-slate-500 dark:text-slate-400",
svg: "text-emerald-600 dark:text-emerald-400",
sql: "text-rose-600 dark:text-rose-400",
img: "text-emerald-600 dark:text-emerald-400",
lock: "text-slate-400 dark:text-slate-500",
};
function ChevronIcon({ className }: { className?: string }) {
return (
<svg viewBox="0 0 16 16" aria-hidden="true" className={className} fill="none">
<path
d="M6 4l4 4-4 4"
stroke="currentColor"
strokeWidth="1.6"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
function FolderIcon({ open, className }: { open: boolean; className?: string }) {
return (
<svg viewBox="0 0 16 16" aria-hidden="true" className={className} fill="none">
{open ? (
<path
d="M1.8 12.6V4.4a.9.9 0 0 1 .9-.9h3.1l1.5 1.6h4.4a.9.9 0 0 1 .9.9v.9M1.8 12.6l1.7-4.7a.9.9 0 0 1 .85-.6h9.7a.6.6 0 0 1 .57.8l-1.5 4.1a.9.9 0 0 1-.85.6H2.7a.9.9 0 0 1-.9-.2Z"
stroke="currentColor"
strokeWidth="1.3"
strokeLinecap="round"
strokeLinejoin="round"
/>
) : (
<path
d="M1.8 12.4V4.4a.9.9 0 0 1 .9-.9h3.1l1.5 1.6h6a.9.9 0 0 1 .9.9v6.4a.9.9 0 0 1-.9.9H2.7a.9.9 0 0 1-.9-.9Z"
stroke="currentColor"
strokeWidth="1.3"
strokeLinecap="round"
strokeLinejoin="round"
/>
)}
</svg>
);
}
function FileIcon({ className }: { className?: string }) {
return (
<svg viewBox="0 0 16 16" aria-hidden="true" className={className} fill="none">
<path
d="M9 1.9H4.4a.9.9 0 0 0-.9.9v10.4a.9.9 0 0 0 .9.9h7.2a.9.9 0 0 0 .9-.9V5.4L9 1.9Z"
stroke="currentColor"
strokeWidth="1.3"
strokeLinejoin="round"
/>
<path
d="M8.9 2v3.4h3.4"
stroke="currentColor"
strokeWidth="1.3"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
function SearchIcon({ className }: { className?: string }) {
return (
<svg viewBox="0 0 16 16" aria-hidden="true" className={className} fill="none">
<circle cx="7.2" cy="7.2" r="4.4" stroke="currentColor" strokeWidth="1.4" />
<path d="M10.6 10.6 13.6 13.6" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" />
</svg>
);
}
function CollapseIcon({ className }: { className?: string }) {
return (
<svg viewBox="0 0 16 16" aria-hidden="true" className={className} fill="none">
<path
d="M4.2 6.6 8 2.9l3.8 3.7M11.8 9.4 8 13.1 4.2 9.4"
stroke="currentColor"
strokeWidth="1.4"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
function CloseIcon({ className }: { className?: string }) {
return (
<svg viewBox="0 0 16 16" aria-hidden="true" className={className} fill="none">
<path
d="M4.4 4.4 11.6 11.6M11.6 4.4 4.4 11.6"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
/>
</svg>
);
}
export default function FileManagerTreeSidebar() {
const reduceMotion = useReducedMotion();
const [open, setOpen] = useState<Set<string>>(
() => new Set(["app", "app/(marketing)", "app/api", "components"])
);
const [selectedId, setSelectedId] = useState<string>("app/(marketing)/pricing/page.tsx");
const [focusId, setFocusId] = useState<string>("app");
const [query, setQuery] = useState<string>("");
const treeRef = useRef<HTMLDivElement | null>(null);
const filtered = useMemo(() => matchTree(TREE, query), [query]);
const effectiveOpen = useMemo(() => {
if (!query.trim()) return open;
return new Set(collectFolderIds(filtered, []));
}, [query, open, filtered]);
const rows = useMemo(
() => flatten(filtered, effectiveOpen, 0, null, []),
[filtered, effectiveOpen]
);
const rowIndex = useMemo(() => {
const map = new Map<string, number>();
rows.forEach((row, i) => map.set(row.node.id, i));
return map;
}, [rows]);
const activeId = rowIndex.has(focusId) ? focusId : rows[0]?.node.id ?? "";
const toggle = useCallback((id: string) => {
setOpen((prev) => {
const next = new Set(prev);
if (next.has(id)) next.delete(id);
else next.add(id);
return next;
});
}, []);
const focusRow = useCallback((id: string) => {
setFocusId(id);
const el = treeRef.current?.querySelector<HTMLDivElement>(`[data-node-id="${CSS.escape(id)}"]`);
el?.focus();
}, []);
const onKeyDown = useCallback(
(event: KeyboardEvent<HTMLDivElement>) => {
const i = rowIndex.get(activeId);
if (i === undefined) return;
const row = rows[i];
const node = row.node;
const isFolder = node.kind === "folder" && !!node.children?.length;
const expanded = isFolder && effectiveOpen.has(node.id);
switch (event.key) {
case "ArrowDown": {
event.preventDefault();
const next = rows[Math.min(i + 1, rows.length - 1)];
if (next) focusRow(next.node.id);
break;
}
case "ArrowUp": {
event.preventDefault();
const prev = rows[Math.max(i - 1, 0)];
if (prev) focusRow(prev.node.id);
break;
}
case "ArrowRight": {
event.preventDefault();
if (isFolder && !expanded) toggle(node.id);
else if (isFolder && expanded) {
const next = rows[i + 1];
if (next && next.parentId === node.id) focusRow(next.node.id);
}
break;
}
case "ArrowLeft": {
event.preventDefault();
if (isFolder && expanded) toggle(node.id);
else if (row.parentId) focusRow(row.parentId);
break;
}
case "Home": {
event.preventDefault();
if (rows[0]) focusRow(rows[0].node.id);
break;
}
case "End": {
event.preventDefault();
const last = rows[rows.length - 1];
if (last) focusRow(last.node.id);
break;
}
case "Enter":
case " ": {
event.preventDefault();
if (isFolder) toggle(node.id);
else setSelectedId(node.id);
break;
}
case "*": {
event.preventDefault();
setOpen((prev) => {
const next = new Set(prev);
rows
.filter((r) => r.parentId === row.parentId && r.node.kind === "folder")
.forEach((r) => next.add(r.node.id));
return next;
});
break;
}
default:
break;
}
},
[rows, rowIndex, activeId, effectiveOpen, toggle, focusRow]
);
const selectedNode = useMemo(() => {
const find = (list: TreeNode[]): TreeNode | null => {
for (const n of list) {
if (n.id === selectedId) return n;
if (n.children) {
const hit = find(n.children);
if (hit) return hit;
}
}
return null;
};
return find(TREE);
}, [selectedId]);
const openCount = effectiveOpen.size;
return (
<section className="relative w-full bg-slate-50 px-5 py-20 sm:px-8 dark:bg-zinc-950">
<style>{`
@keyframes fmtree-caret-in {
from { opacity: 0; transform: translateX(-2px); }
to { opacity: 1; transform: translateX(0); }
}
@keyframes fmtree-pulse-dot {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.45; transform: scale(0.72); }
}
.fmtree-caret-in { animation: fmtree-caret-in 240ms ease-out both; }
.fmtree-pulse-dot { animation: fmtree-pulse-dot 2.4s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
.fmtree-caret-in,
.fmtree-pulse-dot { animation: none !important; }
}
`}</style>
<div className="mx-auto w-full max-w-5xl">
<header className="mb-8">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-indigo-600 dark:text-indigo-400">
Workspace
</p>
<h2 className="mt-2 text-2xl font-semibold tracking-tight text-slate-900 sm:text-3xl dark:text-slate-50">
Project explorer
</h2>
<p className="mt-2 max-w-xl text-sm leading-relaxed text-slate-600 dark:text-slate-400">
A keyboard-navigable folder tree. Arrow keys move and expand, Enter opens,{" "}
<kbd className="rounded border border-slate-300 bg-white px-1 font-sans text-[11px] text-slate-700 dark:border-zinc-700 dark:bg-zinc-900 dark:text-slate-300">
*
</kbd>{" "}
expands every sibling folder.
</p>
</header>
<div className="grid gap-5 lg:grid-cols-[minmax(0,20rem)_minmax(0,1fr)]">
<div className="overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
<div className="flex items-center justify-between gap-2 border-b border-slate-200 px-3 py-2.5 dark:border-zinc-800">
<div className="flex min-w-0 items-center gap-2">
<span className="grid h-6 w-6 shrink-0 place-items-center rounded bg-indigo-600 text-[11px] font-bold text-white dark:bg-indigo-500">
LP
</span>
<span className="truncate text-sm font-medium text-slate-800 dark:text-slate-200">
ledger-portal
</span>
</div>
<button
type="button"
onClick={() => setOpen(new Set())}
disabled={query.trim().length > 0}
className="inline-flex shrink-0 items-center gap-1.5 rounded-md px-2 py-1 text-xs font-medium 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 disabled:pointer-events-none disabled:opacity-40 dark:text-slate-400 dark:hover:bg-zinc-800 dark:hover:text-slate-100 dark:focus-visible:ring-offset-zinc-900"
>
<CollapseIcon className="h-3.5 w-3.5" />
Collapse
</button>
</div>
<div className="border-b border-slate-200 p-2.5 dark:border-zinc-800">
<div className="relative">
<SearchIcon className="pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-slate-400 dark:text-slate-500" />
<label htmlFor="fmtree-search" className="sr-only">
Filter files and folders
</label>
<input
id="fmtree-search"
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Filter by name…"
className="w-full rounded-md border border-slate-200 bg-slate-50 py-1.5 pl-8 pr-8 text-sm text-slate-800 placeholder:text-slate-400 focus-visible:border-indigo-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500/40 dark:border-zinc-700 dark:bg-zinc-950 dark:text-slate-200 dark:placeholder:text-slate-500"
/>
{query.length > 0 ? (
<button
type="button"
onClick={() => setQuery("")}
className="absolute right-1.5 top-1/2 grid h-5 w-5 -translate-y-1/2 place-items-center rounded text-slate-400 transition-colors hover:bg-slate-200 hover:text-slate-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:hover:bg-zinc-800 dark:hover:text-slate-200"
>
<CloseIcon className="h-3 w-3" />
<span className="sr-only">Clear filter</span>
</button>
) : null}
</div>
</div>
<div
ref={treeRef}
role="tree"
aria-label="Project files"
aria-multiselectable={false}
onKeyDown={onKeyDown}
className="max-h-[26rem] overflow-y-auto py-1.5"
>
{rows.length === 0 ? (
<p className="px-4 py-8 text-center text-sm text-slate-500 dark:text-slate-400">
No files match “{query}”.
</p>
) : (
rows.map((row) => {
const { node, depth, setSize, posInSet } = row;
const isFolder = node.kind === "folder" && !!node.children?.length;
const expanded = isFolder ? effectiveOpen.has(node.id) : undefined;
const isSelected = node.id === selectedId;
const isActive = node.id === activeId;
return (
<div
key={node.id}
role="treeitem"
data-node-id={node.id}
aria-level={depth + 1}
aria-setsize={setSize}
aria-posinset={posInSet}
aria-expanded={expanded}
aria-selected={isSelected}
tabIndex={isActive ? 0 : -1}
onFocus={() => setFocusId(node.id)}
onClick={() => {
setFocusId(node.id);
if (isFolder) toggle(node.id);
else setSelectedId(node.id);
}}
style={{ paddingLeft: `${depth * 14 + 8}px` }}
className={[
"group relative mx-1.5 flex cursor-pointer select-none items-center gap-1.5 rounded-md py-[5px] pr-2 text-sm outline-none transition-colors",
"focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1 focus-visible:ring-offset-white dark:focus-visible:ring-offset-zinc-900",
isSelected
? "bg-indigo-50 text-indigo-900 dark:bg-indigo-500/15 dark:text-indigo-100"
: "text-slate-700 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-zinc-800/70",
].join(" ")}
>
{isFolder ? (
<motion.span
aria-hidden="true"
animate={{ rotate: expanded ? 90 : 0 }}
transition={
reduceMotion
? { duration: 0 }
: { type: "spring", stiffness: 520, damping: 32 }
}
className="grid h-3.5 w-3.5 shrink-0 place-items-center text-slate-400 dark:text-slate-500"
>
<ChevronIcon className="h-3.5 w-3.5" />
</motion.span>
) : (
<span aria-hidden="true" className="h-3.5 w-3.5 shrink-0" />
)}
{node.kind === "folder" ? (
<FolderIcon
open={!!expanded}
className="h-4 w-4 shrink-0 text-amber-500 dark:text-amber-400"
/>
) : (
<FileIcon
className={`h-4 w-4 shrink-0 ${node.lang ? LANG_TONE[node.lang] : "text-slate-400"}`}
/>
)}
<span className="truncate">{node.name}</span>
{node.changed ? (
<span
className={[
"fmtree-pulse-dot ml-auto h-1.5 w-1.5 shrink-0 rounded-full",
node.changed === "added" ? "bg-emerald-500" : "bg-amber-500",
].join(" ")}
title={node.changed === "added" ? "Added" : "Modified"}
>
<span className="sr-only">
{node.changed === "added" ? "Added" : "Modified"}
</span>
</span>
) : null}
</div>
);
})
)}
</div>
<div className="flex items-center justify-between border-t border-slate-200 px-3 py-2 text-[11px] text-slate-500 dark:border-zinc-800 dark:text-slate-400">
<span>{rows.length} visible</span>
<span>{openCount} folders open</span>
</div>
</div>
<div className="rounded-xl border border-slate-200 bg-white p-5 shadow-sm sm:p-6 dark:border-zinc-800 dark:bg-zinc-900">
{selectedNode ? (
<div key={selectedNode.id} className={reduceMotion ? "" : "fmtree-caret-in"}>
<div className="flex items-start gap-3">
<span className="mt-0.5 grid h-9 w-9 shrink-0 place-items-center rounded-lg border border-slate-200 bg-slate-50 dark:border-zinc-700 dark:bg-zinc-950">
<FileIcon
className={`h-4 w-4 ${selectedNode.lang ? LANG_TONE[selectedNode.lang] : "text-slate-400"}`}
/>
</span>
<div className="min-w-0">
<h3 className="truncate text-base font-semibold text-slate-900 dark:text-slate-50">
{selectedNode.name}
</h3>
<p className="mt-0.5 break-all font-mono text-xs text-slate-500 dark:text-slate-400">
{selectedId}
</p>
</div>
</div>
<dl className="mt-6 grid grid-cols-2 gap-x-4 gap-y-4 sm:grid-cols-3">
<div>
<dt className="text-[11px] font-medium uppercase tracking-wide text-slate-400 dark:text-slate-500">
Size
</dt>
<dd className="mt-1 text-sm text-slate-800 dark:text-slate-200">
{selectedNode.size ?? "—"}
</dd>
</div>
<div>
<dt className="text-[11px] font-medium uppercase tracking-wide text-slate-400 dark:text-slate-500">
Type
</dt>
<dd className="mt-1 text-sm uppercase text-slate-800 dark:text-slate-200">
{selectedNode.lang ?? selectedNode.kind}
</dd>
</div>
<div>
<dt className="text-[11px] font-medium uppercase tracking-wide text-slate-400 dark:text-slate-500">
Status
</dt>
<dd className="mt-1 text-sm">
{selectedNode.changed === "added" ? (
<span className="inline-flex items-center gap-1.5 rounded-full bg-emerald-50 px-2 py-0.5 text-xs font-medium text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-300">
Added
</span>
) : selectedNode.changed === "modified" ? (
<span className="inline-flex items-center gap-1.5 rounded-full bg-amber-50 px-2 py-0.5 text-xs font-medium text-amber-700 dark:bg-amber-500/15 dark:text-amber-300">
Modified
</span>
) : (
<span className="inline-flex items-center gap-1.5 rounded-full bg-slate-100 px-2 py-0.5 text-xs font-medium text-slate-600 dark:bg-zinc-800 dark:text-slate-300">
Committed
</span>
)}
</dd>
</div>
</dl>
<p className="mt-6 border-t border-slate-200 pt-5 text-sm leading-relaxed text-slate-600 dark:border-zinc-800 dark:text-slate-400">
Select a file in the tree to preview its metadata here. Folder rows toggle on click
or with Enter; the tree keeps a single roving tab stop, so one Tab press reaches it
and arrow keys do the rest.
</p>
</div>
) : (
<p className="text-sm text-slate-500 dark:text-slate-400">
Nothing selected. Pick a file from the tree.
</p>
)}
</div>
</div>
</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 Breadcrumb
Originalfile path breadcrumb with toolbar

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.

