TheĀ SANDBOX

File Tree Render

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. Files consume this as-is, but nested Folders read the value and establish a new, depth-local depth Context.

index.tsx
File.tsx
Folder.tsx
styles.css
index.html
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>