Tree Menu — Free HTML CSS JS Snippet

Collapsible Tree Menu · Navigation · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

max-height: 0→1000px transition on .subtree — smooth expand without JS height calculation
chevron rotate(90deg) driven by .open class on folder node
selectFile(): removes .active from all files, adds to clicked file
collapseAll(): finds all .open folders and closes them in one pass
Progressive indentation: CSS descendant selectors for each nesting level
ARIA: role=tree/treeitem, aria-expanded toggled on open/close
.closed class added to collapsed subtrees on init (from :not(.open) folders)
node-count badge on folder nodes shows child file count

About this UI Snippet

Collapsible Tree Menu — Folder Toggle, max-height Animation, Active File & Collapse All

Screenshot of the Collapsible Tree Menu snippet rendered live

Open a file explorer with two thousand files dumped into one flat list and you'd never find anything — fold the same two thousand into folders-within-folders, and suddenly they're navigable, because at any moment you only have to look at the dozen or so that share your current context. This snippet builds that exact structure: a nested list of folders and files, each folder a click away from expanding or collapsing with a smooth height animation, a chevron that rotates to mirror its state, single-file selection, and a "collapse all" shortcut for when a dozen open folders have turned the sidebar into its own thing to scroll through.

Animating a height nobody can know in advance

CSS transitions height cleanly between two known values, but a folder's expanded height depends entirely on how many files and subfolders happen to be inside it — a number this snippet has no way to know ahead of time. The workaround is max-height: collapsed subtrees sit at max-height: 0 with overflow: hidden, and .open ones jump to max-height: 1000px — not because any real subtree is that tall, but because it's comfortably larger than any realistic one. transition: max-height 0.25s ease animates between the two, and because the browser only ever paints the *actual* content height, the motion reads as if it's measuring the real size, even though it's really racing toward an arbitrary ceiling and stopping early. It's the same trick the Accordion / FAQ snippet leans on to expand its answer panels.

One class name driving two animations at once

toggle() does exactly one meaningful thing: it flips .open on the folder's list item. Everything visible after that — the subtree sliding open via max-height, and the chevron rotating via transform: rotate(90deg) on .tree-node.folder.open .chevron — is pure CSS reacting to that single class change. There's no separate "now rotate the chevron" call to keep in sync with "now expand the subtree": if one fires, so does the other, because both are just selectors watching the same toggle land.

Indentation that comes from nesting, not from counting

Working out how deep a node sits and assigning it a matching padding-left in JavaScript would mean walking the tree and threading a depth counter through every recursive call. This snippet skips that bookkeeping with descendant selectors instead: .subtree .node-row gets one padding value, .subtree .subtree .node-row gets a larger one, and so on — the *selector itself* encodes the nesting depth, so a folder three levels down ends up indented correctly purely because of where its markup physically sits in the document.

Selecting one file, and only one

selectFile() opens by stripping .active from every node that currently carries it — document.querySelectorAll('.tree-node.file.active').forEach(n => n.classList.remove('active')) — and only then adds it to the file just clicked. That "clear everything, then set the one thing" order is the simplest possible way to guarantee a single-selection invariant: there's no special case for "what if two were already active," because the first line makes that state impossible to reach. Right-click a node in a real file explorer, though, and you'd expect a menu of actions to appear — precisely what the Context Menu snippet adds on top of a tree like this one.

Closing everything in one pass

collapseAll() runs the inverse of toggle() across every currently open folder at once — stripping .open, adding .closed to each subtree, and resetting aria-expanded back to "false". It's a short function, but it fixes a real annoyance: after wandering deep into a tree, getting back to a clean overview shouldn't mean clicking a dozen folders shut by hand, one breadcrumb at a time.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Instead of reasoning through every selector by hand, paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to walk through why the subtree max-height is set to a flat 1000px rather than any measured value, and what that arbitrary ceiling means for the closing animation's timing on a very short branch versus a very tall one. It's also worth asking it to optimize the indentation approach — the fixed .subtree .subtree descendant selectors only cover a few nesting levels, so ask what happens (and what to change) for a tree ten levels deep. For extending it, have it add keyboard arrow-key navigation between rows, lazy-loaded children fetched on first expand, or a right-click context menu using the pattern from the separate context menu snippet. Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a collapsible file-tree menu in plain HTML, CSS, and vanilla JavaScript, using only CSS for the expand/collapse animation (no JS height measurement, no libraries).

Requirements:
- A nested unordered list where each folder is a list item containing a clickable row and a nested ul.subtree of its own children; each file is a leaf list item with no subtree.
- Collapsed subtrees must be styled with max-height: 0 and overflow: hidden, and open ones with a fixed max-height comfortably larger than any realistic subtree (e.g. 1000px), with a CSS transition on max-height so toggling reads as a smooth slide rather than an instant snap — the actual rendered height must still be determined by the real content, not by the max-height ceiling.
- A single toggle() function must add or remove one "open" class on the folder's list item; a chevron icon must rotate 90 degrees purely via a CSS selector reacting to that same class, with no separate JS call to rotate it.
- Indentation for each nesting level must come from CSS descendant selectors (e.g. .subtree .node-row, .subtree .subtree .node-row) rather than inline styles or a JS-computed depth value.
- Clicking a file row must clear the "active" class from every other file first, then add it to the clicked file, guaranteeing only one file is ever marked active.
- A "collapse all" button must find every currently open folder and close them all in a single pass, resetting aria-expanded to false on each.
- Use role="tree" and role="treeitem" with aria-expanded reflecting each folder's state.

Step by step

How to Use

  1. 1
    Click folder names to expand or collapseClicking any folder row toggles it open or closed with a smooth max-height animation. The chevron rotates 90 degrees to indicate the state. Click the collapse-all icon to close all open folders at once.
  2. 2
    Click file names to select themClicking a file highlights it with an indigo background (active state). Only one file can be active at a time. The active file remains highlighted until another is clicked.
  3. 3
    Update the tree structure with your own filesAdd .tree-node.folder li elements for directories and .tree-node.file li elements for files. Nest .subtree ul lists inside folder nodes. Update .node-icon-leaf emojis or SVG icons to match your file types.
  4. 4
    Add more nesting levelsThe CSS handles up to 3 nesting levels via .subtree selectors. For deeper nesting, add .subtree .subtree .subtree .node-row { padding-left: 54px; } and extend the pattern.
  5. 5
    Add right-click context menuListen for contextmenu on each .node-row: row.addEventListener("contextmenu", e => { e.preventDefault(); showContextMenu(e, node); }). See the Context Menu snippet in the Navigation category for the full implementation.
  6. 6
    Export in your formatClick "HTML" for a standalone file, "JSX" for a React component with an items array prop, or "Tailwind" for a React + Tailwind CSS version.

Real-world uses

Common Use Cases

IDE and code editor file tree sidebar
The file tree pattern is the primary navigation in VS Code, JetBrains, and every modern code editor. Use this snippet as the foundation for a browser-based code editor sidebar.
Design system component and asset browser
Design systems have hierarchical component categories (Forms > Inputs > Text Input). The tree menu navigates the hierarchy efficiently. Active file selection highlights the currently viewed component.
Documentation and knowledge base sidebar navigation
Technical documentation sites (like Stripe, Tailwind CSS) use collapsible tree navigation for API references and guides. The collapse-all button is essential when users have many sections open.
Settings panel with nested categories
Admin panel settings have parent categories (Security > Authentication > Password Policy). The tree menu handles arbitrary nesting depth with consistent visual indentation and smooth animations.
Study max-height CSS animation for unknown heights
The max-height trick for animating unknown content heights (folder subtrees of unpredictable size) is a fundamental CSS technique. This snippet demonstrates the pattern clearly — set a max-height larger than any realistic content, animate from 0 to that value.
File manager and cloud storage browser UI
Cloud storage UIs (Dropbox, Google Drive) use tree views for folder navigation. Folder expand shows children; collapse hides them. Active selection drives a content panel showing the selected folder or file details.

Got questions?

Frequently Asked Questions

Subtrees have max-height: 1000px when open (which accommodates any realistic tree depth) and max-height: 0 when closed. CSS transition: max-height 0.25s ease animates between these values. The browser interpolates from 0 to 1000px — even though the actual content might only be 120px, the transition appears to stop when the content is fully visible. The trade-off: the closing animation is always 0.25s regardless of actual height, which can feel slightly off for very deep trees.

Define a buildTree(nodes, container) function that iterates an array of {name, type, children} objects. For folders: create an li with .tree-node.folder, add a .node-row div, create a .subtree ul, and recurse. For files: create an li with .tree-node.file and a .node-row.leaf div. Append each li to the container. Call toggle(row) event listeners after generation. This pattern renders any nested data structure as a tree UI.

Add a keydown listener to the tree: for ArrowDown, find the next visible node and focus it. For ArrowUp, find the previous visible node. For ArrowRight, expand the current folder. For ArrowLeft, collapse the current folder or move to its parent. For Enter, select the current item. Track the focused node with a data-focused attribute. Apply focus styles with .node-row:focus-visible rules.

Click "JSX" to download. Define a TreeNode component that accepts name, type, and children props. Manage open state with useState(initiallyOpen). The subtree div has style={{ maxHeight: open ? 1000 : 0, overflow: "hidden", transition: "max-height 0.25s ease" }}. For file selection, lift the selectedFile state to the parent Tree component and pass setSelected as a callback.