Skip to main content

Component Trees

· 9 min read
Docux
Curious explorer, a bit of a mad experimenter, and a bit of a contributor.

Developer Development AI 50% License: MIT

Introduction

The Trees component allows you to display interactive file and folder trees directly in your Docusaurus pages. Perfect for documenting project structure, explaining architecture, or presenting code examples.

Project with Icons

src
package.json

✨ Key Features

  • Intuitive syntax: Use natural nested JSX tags
  • Collapsible folders: Click to expand/collapse folders
  • Two display modes:
    • Native emojis by default (📁 📄)
    • Custom icons via LogoIcon (Iconify)
  • Dark mode: Full dark theme support
  • Accessible: Keyboard navigation and smooth animations
  • Optional badges: Add extra information

Installation

Copy/Create Files

Create the following folder structure in your Docusaurus project:

(Note that I'm intentionally not using my component here, ha ha!)

src/components/Trees/
├── index.js # Main Trees component
├── Folder/
│ └── index.js # Folder component
├── File/
│ └── index.js # File component
├── utils/
│ └── TreeItem.js # Shared internal component
└── styles.module.css # CSS styles

The component follows a modular architecture inspired by React best practices

Component Architecture

File Trees/index.js

Main Component

  • Tree container
  • Displays an optional title
  • Manages children rendering (Folder/File)
  • Applies initial indentation level (0)
import React, { Children, isValidElement } from "react";
import PropTypes from "prop-types";
import styles from "./styles.module.css";

/**
* Trees Component - Main component to display a tree structure
* Uses intuitive JSX syntax with nested tags
*/
export default function Trees({ title, children }) {
return (
<div className={styles.treeContainer}>
{title && <h3 className={styles.treeTitle}>{title}</h3>}
<div className={styles.treeContent}>
{Children.map(children, (child, index) => {
if (isValidElement(child)) {
return React.cloneElement(child, {
key: index,
level: 0,
});
}
return null;
})}
</div>
</div>
);
}

Trees.propTypes = {
/** Optional tree title */
title: PropTypes.string,
/** Tree content (Folder and File components) */
children: PropTypes.node.isRequired,
};

Trees/Folder/index.js

Folder Component

  • Represents a folder in the tree
  • Wrapper around TreeItem with defaultEmoji="📁"
  • Can contain children (sub-folders and files)
  • Props: label, icon, expanded, badge, iconSize
import React from "react";
import PropTypes from "prop-types";
import TreeItem from "../utils/TreeItem";

/**
* Folder Component - Represents a folder in the tree structure
*/
export default function Folder({ icon, iconSize = 24, badge, label, expanded = false, children, level = 0 }) {
return (
<TreeItem
icon={icon}
iconSize={iconSize}
badge={badge}
label={label}
expanded={expanded}
level={level}
defaultEmoji="📁"
>
{children}
</TreeItem>
);
}

Folder.propTypes = {
/** Icon name (LogoIcon) to display */
icon: PropTypes.string,
/** Icon size in pixels */
iconSize: PropTypes.number,
/** Optional badge to display */
badge: PropTypes.string,
/** Folder name to display */
label: PropTypes.string.isRequired,
/** Initial state: expanded (true) or collapsed (false) */
expanded: PropTypes.bool,
/** Folder content (sub-folders and files) */
children: PropTypes.node,
/** Nesting level (managed automatically) */
level: PropTypes.number,
};

Trees/File/index.js

File Component

  • Represents a file in the tree
  • Wrapper around TreeItem with defaultEmoji="📄"
  • Cannot contain children
  • Props: label, icon, badge, iconSize
import React from "react";
import PropTypes from "prop-types";
import TreeItem from "../utils/TreeItem";

/**
* File Component - Represents a file in the tree structure
*/
export default function File({ icon, iconSize = 20, badge, label, level = 0 }) {
return (
<TreeItem
icon={icon}
iconSize={iconSize}
badge={badge}
label={label}
expanded={false}
level={level}
defaultEmoji="📄"
/>
);
}

File.propTypes = {
/** Icon name (LogoIcon) to display */
icon: PropTypes.string,
/** Icon size in pixels */
iconSize: PropTypes.number,
/** Optional badge to display */
badge: PropTypes.string,
/** File name to display */
label: PropTypes.string.isRequired,
/** Nesting level (managed automatically) */
level: PropTypes.number,
};

File Trees/utils/TreeItem.js

Internal Utility Component

Shared component that factors all common logic:

  • ✅ Expansion state management (useState)
  • ✅ Valid React children detection
  • ✅ Icon display (LogoIcon or native emojis)
  • ✅ Recursive sub-element rendering
  • ✅ Dynamic indentation calculation (level * 20px)
  • ✅ User interaction (click to expand/collapse)

Icon display logic:

  1. If icon is provided → Uses LogoIcon (Iconify)
  2. If icon is absent → Uses defaultEmoji (📁 or 📄)
  3. If icon={null} → No icon
import React, { useState, Children, isValidElement } from "react";
import styles from "../styles.module.css";
import LogoIcon from "../../LogoIcon";

/**
* TreeItem Component - Represents an individual tree element
* Internal component shared by Folder and File
*/
export default function TreeItem({ icon, iconSize = 24, badge, label, expanded = false, children, level = 0, defaultEmoji }) {
const [isExpanded, setIsExpanded] = useState(expanded);

// Filter valid React children (other Folder or File)
const childElements = Children.toArray(children).filter(child => isValidElement(child));
const hasChildren = childElements.length > 0;

const toggleExpand = () => {
if (hasChildren) {
setIsExpanded(!isExpanded);
}
};

return (
<div className={styles.treeNode}>
<div
className={`${styles.nodeContent} ${hasChildren ? styles.hasChildren : ""}`}
style={{ paddingLeft: `${level * 20}px` }}
onClick={toggleExpand}
>
{/* Expand/collapse icon */}
{hasChildren && (
<span className={styles.expandIcon}>
{isExpanded ? "▼" : "▶"}
</span>
)}

{/* Node icon with label (via LogoIcon or native emoji) */}
{icon ? (
<span className={styles.nodeIcon}>
<LogoIcon name={icon} size={iconSize} />
<span className={styles.nodeLabel}>{label}</span>
</span>
) : defaultEmoji ? (
<span className={styles.nodeIcon}>
<span style={{ fontSize: `${iconSize}px`, lineHeight: 1 }}>{defaultEmoji}</span>
<span className={styles.nodeLabel}>{label}</span>
</span>
) : (
<span className={styles.nodeLabel}>{label}</span>
)}

{/* Optional badge */}
{badge && (
<span className={styles.nodeBadge}>{badge}</span>
)}
</div>

{/* Children (recursive) */}
{hasChildren && isExpanded && (
<div className={styles.nodeChildren}>
{childElements.map((child, index) =>
React.cloneElement(child, {
key: index,
level: level + 1,
})
)}
</div>
)}
</div>
);
}

File styles.module.css

Component CSS Styles File

Single CSS shared by all components with:

  • CSS variables for colors and spacing
  • Dark mode support ([data-theme='dark'])
  • Smooth animations for transitions
  • Modular classes (.treeContainer, .nodeContent, etc.)
.treeContainer {
border: 1px solid var(--ifm-color-emphasis-300);
border-radius: 3px;
padding: 16px;
margin: 20px 0;
background-color: var(--ifm-background-surface-color);
}

.treeTitle {
margin: 0 0 16px 0;
font-size: 1.2rem;
font-weight: 600;
color: var(--ifm-color-primary);
border-bottom: 2px solid var(--ifm-color-emphasis-300);
padding-bottom: 8px;
}

.treeContent {
font-family: var(--ifm-font-family-monospace);
}

.treeNode {
margin: 0;
position: relative;
}

.nodeContent {
display: flex;
align-items: center;
padding: 6px 8px;
padding-left: 8px;
border-radius: 4px;
transition: background-color 0.2s ease;
cursor: default;
position: relative;
}

.nodeContent.hasChildren {
cursor: pointer;
}

.nodeContent:hover {
background-color: var(--ifm-color-emphasis-100);
}

.expandIcon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
min-width: 20px;
height: 20px;
margin-right: 4px;
font-size: 10px;
color: var(--ifm-color-emphasis-600);
transition: transform 0.2s ease;
user-select: none;
flex-shrink: 0;
}

.nodeIcon {
display: flex;
align-items: center;
justify-content: flex-start;
flex: 1;
gap: 8px;
}

/* Add offset for files (without expandIcon) for alignment */
.nodeContent:not(.hasChildren) .nodeIcon {
margin-left: 24px;
}

.nodeLabel {
font-size: 0.95rem;
color: var(--ifm-color-content);
user-select: none;
line-height: 1;
white-space: nowrap;
}

.nodeBadge {
display: inline-flex;
align-items: center;
padding: 2px 8px;
font-size: 0.75rem;
font-weight: 600;
border-radius: 12px;
background-color: var(--ifm-color-primary);
color: white;
user-select: none;
}

.nodeChildren {
margin-left: 20px;
border-left: 1px solid var(--ifm-color-emphasis-300);
padding-left: 8px;
animation: slideDown 0.2s ease-out;
}

@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

/* Dark mode styles */
[data-theme='dark'] .treeContainer {
border-color: var(--ifm-color-emphasis-300);
background-color: var(--ifm-background-surface-color);
}

[data-theme='dark'] .nodeContent:hover {
background-color: var(--ifm-color-emphasis-200);
}

[data-theme='dark'] .nodeLabel {
color: var(--ifm-color-content);
}

/* Responsive */
@media (max-width: 768px) {
.treeContainer {
padding: 12px;
}

.nodeLabel {
font-size: 0.9rem;
}

.nodeIcon {
transform: scale(0.9);
}
}

MDX Integration

Register in MDXComponents

Open the file src/theme/MDXComponents.js and add the imports:

src/theme/MDXComponents.js
import React from 'react';
import MDXComponents from '@theme-original/MDXComponents';
import Trees from '@site/src/components/Trees';
import Folder from '@site/src/components/Trees/Folder';
import File from '@site/src/components/Trees/File';

export default {
...MDXComponents,
Trees,
Folder,
File
};

Use in Your MDX Files

Once registered in MDXComponents, the components are automatically available in all your .mdx files: See official documentation

blog/my-article.mdx
## Project Structure

<Trees title="My Project">
<Folder label="src">
<File label="index.js" />
<File label="App.jsx" />
</Folder>
<File label="package.json" />
</Trees>

Usage Examples

Example 1: Simple Tree with Native Emojis

Without LogoIcon icons, the component automatically uses native emojis (📁 for folders, 📄 for files).

Simple Project

📁src
📄index.js
📄App.jsx
📁public
📄index.html
📄package.json
📄README.md
<Trees title="Simple Project">
<Folder label="src" expanded={true}>
<File label="index.js" />
<File label="App.jsx" />
</Folder>
<Folder label="public" expanded={true}>
<File label="index.html" />
</Folder>
<File label="package.json" />
<File label="README.md" />
</Trees>

Example 2: Collapsed Folders by Default

Use the expanded={false} prop to have folders collapsed on load.

Compact Tree

📁node_modules
📁src
<Trees title="Compact Tree">
<Folder label="node_modules" expanded={false}>
<Folder label="react">
<File label="index.js" />
</Folder>
</Folder>
<Folder label="src">
<File label="App.jsx" />
</Folder>
</Trees>

Example 3: With LogoIcon Icons (Simple)

Add the icon prop to use Iconify icons via LogoIcon.

Project with Icons

src
package.json
<Trees title="Project with Icons">
<Folder label="src" icon="unjs:mkdist">
<File label="index.js" icon="logos:javascript" />
<File label="App.jsx" icon="logos:react" />
<File label="styles.css" icon="logos:css-3" />
</Folder>
<File label="package.json" icon="vscode-icons:file-type-node" />
</Trees>

Example 4: Complex Tree with Badges

Use the badge prop to display additional information.

Backend Architecture

src
tests
.env
package.json
<Trees title="Backend Architecture">
<Folder label="src" icon="vscode-icons:default-folder">
<Folder label="controllers" icon="vscode-icons:folder-type-controller" badge="5 files">
<File label="userController.js" icon="vscode-icons:file-type-js" />
<File label="authController.js" icon="vscode-icons:file-type-js" />
</Folder>
<Folder label="models" icon="vscode-icons:folder-type-model" badge="3 files">
<File label="User.js" icon="vscode-icons:file-type-js" />
</Folder>
<File label="server.js" icon="vscode-icons:file-type-node" badge="main" />
</Folder>
<File label=".env" icon="vscode-icons:file-type-dotenv" />
</Trees>

Example 5: Complete Docusaurus Project

Realistic example of a Docusaurus structure.

Docusaurus Project

blog
docs
src
static
docusaurus.config.js
package.json
sidebars.js
<Trees title="Docusaurus Project">
<Folder label="blog" icon="vscode-icons:folder-type-docs">
<Folder label="2025" icon="vscode-icons:default-folder">
<File label="index.mdx" icon="vscode-icons:file-type-mdx" />
</Folder>
</Folder>
<Folder label="src" icon="vscode-icons:default-folder">
<Folder label="components" icon="vscode-icons:folder-type-component">
<Folder label="Trees" icon="vscode-icons:default-folder" badge="new">
<File label="index.js" icon="vscode-icons:file-type-reactjs" />
</Folder>
</Folder>
</Folder>
<File label="docusaurus.config.js" icon="vscode-icons:file-type-docusaurus" />
</Trees>

Component Props

Component <Trees>

PropTypeDefaultDescription
titlestring-Optional tree title
childrennode-Content (Folder and File components)

Component <Folder>

PropTypeDefaultDescription
labelstringRequiredFolder name
iconstring-Iconify icon name (via LogoIcon)
iconSizenumber24Icon size in pixels
badgestring-Optional badge to display
expandedbooleanfalseInitial state (expanded/collapsed)
childrennode-Sub-folders and files

Component <File>

PropTypeDefaultDescription
labelstringRequiredFile name
iconstring-Iconify icon name (via LogoIcon)
iconSizenumber20Icon size in pixels
badgestring-Optional badge to display

Usage With and Without LogoIcon

Mode 1: Native Emojis (without LogoIcon)

Advantages:

  • ✅ No external dependencies
  • ✅ Works immediately
  • ✅ Lightweight and performant
<Trees title="Simple">
<Folder label="Documents">
<File label="report.pdf" />
</Folder>
</Trees>

Result: 📁 Documents / 📄 report.pdf

Mode 2: LogoIcon Icons (with Iconify)

Prerequisites: Have the LogoIcon component installed in your project.

Advantages:

  • ✅ Specific icons by file type
  • ✅ Over 150,000 icons available
  • ✅ Professional rendering
<Trees title="With Icons">
<Folder label="src" icon="vscode-icons:default-folder">
<File label="App.jsx" icon="vscode-icons:file-type-reactjs" />
</Folder>
</Trees>

Mode 3: No Icon at All

Pass icon={null} to completely disable icons.

<File label="file.txt" icon={null} />

Dark Mode Support

The component automatically supports Docusaurus dark mode through CSS variables:

/* Light mode */
.nodeContent {
background-color: var(--ifm-background-surface-color);
color: var(--ifm-font-color-base);
}

/* Dark mode (automatic) */
[data-theme='dark'] .nodeContent:hover {
background-color: rgba(255, 255, 255, 0.05);
}

Best Practices

✅ Do

  • Use expanded={false} for large folders (e.g., node_modules)
  • Add badges for important information
  • Use consistent icons (same Iconify pack)
  • Limit depth to 4-5 levels maximum

❌ Don't

  • Don't create overly deep trees (readability)
  • Avoid mixing native emojis and LogoIcon in the same tree
  • Don't overuse badges (visual pollution)

Conclusion

The Trees component offers a simple and flexible solution for displaying trees in Docusaurus:

  • Two modes: native emojis or custom icons
  • Interactive: collapsible/expandable folders
  • Dark mode natively supported
  • Accessible and performant
  • Modular architecture easy to maintain

Perfect for:

  • Documenting project structures
  • Presenting file architectures
  • Creating installation guides
  • Illustrating code examples

Try it in your next article and let me know your feedback! 🚀

Related posts

Retour en haut