This is an automatically nesting React folder renderer. The structure is declared in a declarative manner using the Folder
and File
components.
The current depth is maintained in a Context. Many contexts, actually. File
s consume this as-is, but nested Folder
s read the value and establish a new, depth-local depth Context.
const FolderDepthCtx = React.createContext(0); function Tree({ children }) { return <div>{children}</div>; } ReactDOM.render( <Tree> <Folder name="components" defaultExpanded> <Folder name="typography" defaultExpanded> <File name="paragraph.js" /> <File name="code.js" /> <File name="heading.js" /> </Folder> <File name="button.js" /> <File name="avatar.js" /> </Folder> <Folder name="pages"> <File name="dashboard.js" /> <File name="about.js" /> <File name="index.js" /> </Folder> <File name="README.md" /> <File name=".gitignore" /> </Tree>, document.querySelector("#root"), );
function File({ children, className, name }) { const depth = React.useContext(FolderDepthCtx); return ( <div className="file" data-depth={depth}> <span className="material-symbols-outlined file-icon"> description </span> {name} </div> ); }
function Folder({ children, className, defaultExpanded = false, name }) { const [expanded, setExpanded] = React.useState(defaultExpanded); const depth = React.useContext(FolderDepthCtx); return ( <FolderDepthCtx.Provider value={depth + 1}> <div className="folder" data-depth={depth}> <div> <span className={ "material-symbols-outlined " + (expanded ? "collapse" : "expand") + "-icon" } onClick={() => setExpanded(!expanded)} > {expanded ? "remove" : "add"} </span> <span className="material-symbols-outlined folder-icon"> folder </span> {name} </div> {expanded && <div>{children}</div>} </div> </FolderDepthCtx.Provider> ); }
body { padding: 24px; } .folder { position: relative; padding-top: 4px; padding-bottom: 4px; font-size: 12px; } .material-symbols-outlined { font-size: 12px; vertical-align: middle; } .folder .material-symbols-outlined.collapse-icon, .folder .material-symbols-outlined.expand-icon { position: absolute; top: 7px; left: -16px; font-size: 10px; background: white; color: black; border-radius: 2px; cursor: pointer; } .folder:not(*[data-depth="0"]) { margin-left: 8px; border-left: 1px solid white; padding-left: 12px; } .folder:not(*[data-depth="0"]) .material-symbols-outlined.collapse-icon, .folder:not(*[data-depth="0"]) .material-symbols-outlined.expand-icon { left: -5px; } .file { padding-top: 4px; padding-bottom: 4px; font-size: 12px; } .file:not([data-depth="0"]) { margin-left: 8px; border-left: 1px solid white; padding-left: 16px; } .folder .material-symbols-outlined.folder-icon, .file .material-symbols-outlined.file-icon { font-size: 16px; margin-right: 16px; }
<div id="root"></div>