Build an Interactive WBS Viewer
Build a browser-based WBS tree with collapsible nodes and cost rollup.
Introduction
30 Second Summary
Every big project starts as an overwhelming list of tasks. The secret to managing complexity is breaking work into smaller, visual pieces you can actually see and track.
In this project, you will build an interactive Work Breakdown Structure viewer using HTML, CSS, and JavaScript. Your viewer displays a sample project as a clickable tree diagram where costs automatically roll up from tasks to phases.
What You'll Build
A browser-based WBS viewer that renders a "Website Redesign" project as a top-down tree with expandable nodes showing task names, costs, and progress.
By the end of this project, you'll have:
- A working interactive WBS tree rendered in your browser with clickable nodes that expand and collapse.
- A JavaScript data model representing hierarchical project breakdown with cost and progress tracking.
- A reusable HTML/CSS/JS template you can adapt for any project's WBS.
- Secret Mission: Color-coded progress bars and a live summary panel with automatic cost and completion calculations.
Are there any prerequisites?
None. This guide includes full setup for all required tools. You just need a computer running macOS or Windows.
Before We Start
You're about to build an interactive WBS (Work Breakdown Structure) viewer. A WBS is a project management tool that breaks a large project into smaller, manageable pieces arranged in a hierarchy.
Before you can start building, you need a code editor and a project folder. In this step, you'll set up VS Code, create the project files, and install a browser extension for live reloading.
In this step, get ready to:
- Install VS Code.
- Create the wbs-viewer project folder with starter files.
- Set up Live Server for automatic browser refresh.
Install VS Code
VS Code is a free code editor from Microsoft. You'll use it to write all the HTML, CSS, and JavaScript for your WBS viewer.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Visual Studio Code and press Enter.
✔️ VS Code opens
You're all set. Keep VS Code open and move on to the next substep.
ⓧ I can't find VS Code
- Open your browser and go to code.visualstudio.com.
- Click the download button for your operating system.
- Run the installer and follow the prompts.
- Once installed, open VS Code using the search method above.
Create the project folder and files
Every web project needs a home folder containing its files. You'll create a folder called wbs-viewer on your Desktop with three empty starter files inside.
- Open the integrated terminal in VS Code by pressing Ctrl+` (backtick key, below Escape).
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir wbs-viewer
cd wbs-viewer
- Create the three starter files by running this command:
macOS
touch index.html style.css script.js
Windows
New-Item -ItemType "File" -Path "index.html", "style.css", "script.js"
- Confirm the files exist by running:
ls
You should see index.html, style.css, and script.js in the output.
Not seeing all three files?
Make sure you ran the file creation command from inside the wbs-viewer folder. If you see files outside the folder, delete them and run cd ~/Desktop/wbs-viewer before trying again.
help me create files in my project folder
Open the folder in VS Code and install Live Server
Live Server is a VS Code extension that launches a local web server and auto-refreshes your browser every time you save a file. This means you'll see your changes instantly without manually refreshing.
- In VS Code, click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the wbs-viewer folder, and click Open.
You should see your three files listed in the Explorer sidebar on the left.
- Open the Extensions panel by pressing Cmd+Shift+X (macOS) or Ctrl+Shift+X (Windows).
- Type Live Server in the search bar at the top of the panel.
- Find the result by Ritwick Dey and click Install.
How do I know Live Server is installed?
Once installed, you'll see a "Go Live" button appear in the bottom-right corner of the VS Code status bar. You'll use this button in the next step to launch your page in the browser.
Don't see the Go Live button?
Make sure you opened a folder (not just a single file) in VS Code. Live Server only activates when a workspace folder is open.
Try closing and reopening VS Code after installing the extension. If the button still doesn't appear, check that the extension shows as "Enabled" in the Extensions panel.
help me get Live Server working in VS Code
Your editor is ready and your project files are in place. Next up, you'll write the HTML shell that gives your WBS viewer a page to live on.
Build the HTML Page Shell
Your development environment is set up and ready to go. Now it's time to create the foundation of your WBS viewer.
Every web page starts with an HTML file that defines the page structure. In this step, you'll build the HTML shell that connects your stylesheet, your script, and provides the container where the WBS tree will eventually render.
In this step, get ready to:
- Set up the HTML boilerplate with links to your stylesheet and script files.
- Add a page heading and the container element for the WBS tree.
- Preview the page in Chrome using Live Server to confirm it loads.
Create the HTML document structure
An HTML file is the skeleton of any web page. It tells the browser what content to display and which other files (stylesheets and scripts) to load.
- In VS Code, click on index.html in the Explorer sidebar to open it.
- Add the following code to the empty file:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WBS Viewer</title>
<!-- Link the stylesheet for tree layout and node styling -->
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Work Breakdown Structure</h1>
<!-- Container where the WBS tree will be rendered by JavaScript -->
<div id="wbs-root"></div>
<!-- Load the script after the DOM elements are ready -->
<script src="script.js"></script>
</body>
</html>
What does this code do?
The <head> section sets up metadata (character encoding and responsive scaling) and links your style.css file so the browser knows where to find your styles.
The <body> contains an <h1> heading for the page title and a <div id="wbs-root"> element. This empty div is where your JavaScript will build the entire WBS tree later.
The <script> tag is placed at the bottom of the body so the HTML elements load before the script tries to access them.
- Save index.html by pressing Cmd+S (macOS) or Ctrl+S (Windows).
Preview the page with Live Server
Live Server launches a local web server that automatically refreshes your browser whenever you save a file. This gives you instant feedback as you build.
- Right-click anywhere inside index.html in the editor.
- Select Open with Live Server from the context menu.
Chrome should open automatically and display a page with the heading "Work Breakdown Structure" at the top. The rest of the page is blank because you haven't added any tree content yet.
Don't see the heading in your browser?
- Make sure you saved index.html before right-clicking. Unsaved changes won't appear in the browser.
- Check that you right-clicked inside the index.html editor tab, not a different file.
- If Chrome didn't open, check the bottom-right corner of VS Code for a "Port: 5500" indicator. You can also manually open http://127.0.0.1:5500 in Chrome.
Why isn't my HTML page loading in Live Server?
✔️ Awesome, I've got everything!
Great. Make sure you've saved index.html and can see the heading in your browser before moving on.
ⓧ I'd like to double check the full code
Here is the complete index.html file at this point. Compare it to yours and make sure everything matches.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WBS Viewer</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Work Breakdown Structure</h1>
<div id="wbs-root"></div>
<script src="script.js"></script>
</body>
</html>
Your page shell is in place. Next up, you'll define the WBS data model in JavaScript so there's actual project data to display in that empty container.
Define the WBS Data Model in JavaScript
Your page is set up and loading in the browser. Now you need data for the tree to display.
A Work Breakdown Structure breaks a project into layers: the project itself sits at the top, phases sit below it, and individual work packages sit at the bottom. In this step, you'll model a sample "Website Redesign" project as a nested JavaScript object that captures this hierarchy.
In this step, get ready to:
- Create a nested JavaScript object representing the WBS hierarchy.
- Verify the data appears in Chrome DevTools.
Create the WBS data object
Each node in a WBS has a name, a cost, a progress percentage (0 to 100), and an array of children. Leaf nodes (the actual work packages) carry real cost and progress values. Parent nodes have null for cost and progress because those values will be calculated from their children later.
- In VS Code, open script.js from the Explorer sidebar.
- Add the following code to define the project data and log it to the console:
const wbsData = {
name: "Website Redesign",
cost: null,
progress: null,
children: [
{
name: "Planning",
cost: null,
progress: null,
children: [
{ name: "Requirements Gathering", cost: 3000, progress: 100, children: [] },
{ name: "Stakeholder Interviews", cost: 2000, progress: 80, children: [] },
{ name: "Project Charter", cost: 1000, progress: 60, children: [] }
]
},
{
name: "Design",
cost: null,
progress: null,
children: [
{ name: "Wireframes", cost: 4000, progress: 70, children: [] },
{ name: "Visual Mockups", cost: 5000, progress: 30, children: [] },
{ name: "Prototype", cost: 3000, progress: 10, children: [] }
]
},
{
name: "Development",
cost: null,
progress: null,
children: [
{ name: "Frontend Build", cost: 8000, progress: 20, children: [] },
{ name: "Backend API", cost: 6000, progress: 15, children: [] },
{ name: "Database Setup", cost: 2000, progress: 50, children: [] }
]
},
{
name: "Testing & Launch",
cost: null,
progress: null,
children: [
{ name: "QA Testing", cost: 3000, progress: 0, children: [] },
{ name: "User Acceptance", cost: 2000, progress: 0, children: [] },
{ name: "Deployment", cost: 1000, progress: 0, children: [] }
]
}
]
};
console.log(wbsData);
What does this code do?
This object models 3 levels of a WBS hierarchy:
- Level 1 (root): The project itself, "Website Redesign".
- Level 2 (phases): Four major phases like "Planning" and "Design". These have cost: null because their costs will roll up from children.
- Level 3 (work packages): Individual tasks like "Wireframes" with real cost and progress values. Empty children: [] marks them as leaf nodes.
The console.log(wbsData) at the end lets you inspect the full object in your browser's developer tools to confirm everything loaded correctly.
- Save script.js.
- In Chrome, right-click anywhere on the page and select Inspect to open Chrome DevTools.
- Click the Console tab at the top of DevTools.
- You should see the wbsData object logged. Click the disclosure triangle next to it to expand and explore the nested children.
Don't see anything in the console?
- Make sure you saved script.js. A dot next to the filename in VS Code means unsaved changes.
- Check the Console tab for red error messages. A missing comma or bracket in the object will cause a syntax error that prevents the log from running.
- Confirm your index.html has <script src="script.js"></script> just before </body>. Without this link, the browser never loads your JavaScript file.
Help me debug why my console.log isn't showing the wbsData object in Chrome DevTools.
What is the 100% rule in WBS?
In WBS methodology, all child nodes must account for 100% of their parent's scope. That is why the parent nodes have cost: null. Their cost is entirely derived from summing their children. You will implement this rollup calculation in a later step.
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
Here is the complete script.js at this point:
const wbsData = {
name: "Website Redesign",
cost: null,
progress: null,
children: [
{
name: "Planning",
cost: null,
progress: null,
children: [
{ name: "Requirements Gathering", cost: 3000, progress: 100, children: [] },
{ name: "Stakeholder Interviews", cost: 2000, progress: 80, children: [] },
{ name: "Project Charter", cost: 1000, progress: 60, children: [] }
]
},
{
name: "Design",
cost: null,
progress: null,
children: [
{ name: "Wireframes", cost: 4000, progress: 70, children: [] },
{ name: "Visual Mockups", cost: 5000, progress: 30, children: [] },
{ name: "Prototype", cost: 3000, progress: 10, children: [] }
]
},
{
name: "Development",
cost: null,
progress: null,
children: [
{ name: "Frontend Build", cost: 8000, progress: 20, children: [] },
{ name: "Backend API", cost: 6000, progress: 15, children: [] },
{ name: "Database Setup", cost: 2000, progress: 50, children: [] }
]
},
{
name: "Testing & Launch",
cost: null,
progress: null,
children: [
{ name: "QA Testing", cost: 3000, progress: 0, children: [] },
{ name: "User Acceptance", cost: 2000, progress: 0, children: [] },
{ name: "Deployment", cost: 1000, progress: 0, children: [] }
]
}
]
};
console.log(wbsData);
Your data model is locked in and verified. Next up, you'll write a recursive function that turns this object into a visible tree of HTML elements in the browser.
Render the Tree with JavaScript
Your WBS data model is defined, but it just sits silently in memory. To make it visual, you need to transform each node in the hierarchy into an actual HTML element on the page.
In this step, you will write two recursive functions. One calculates rolled-up costs from child nodes to parent nodes. The other builds the visible tree by creating DOM elements for every node and nesting children inside their parents.
In this step, get ready to:
- Write a cost rollup function that fills in parent-node costs from their children.
- Write a recursive render function that turns each node into clickable HTML.
- Verify the tree structure appears in the browser.
Calculate cost rollups for parent nodes
In your data model, parent nodes like "Planning" and "Design" have their cost set to null. Their costs should be the sum of all their children. You need a function that walks the tree bottom-up and fills in those values before you try to display them.
- In script.js, find and delete the console.log(wbsData); line at the bottom of the file.
- In its place, add the calculateRollups function by copying this code:
function calculateRollups(node) {
// Base case: leaf nodes already have cost and progress values
if (node.children.length === 0) {
return { cost: node.cost, progress: node.progress };
}
// Accumulate totals from all children
let totalCost = 0;
let weightedProgress = 0;
node.children.forEach(child => {
const childRollup = calculateRollups(child);
totalCost += childRollup.cost;
weightedProgress += childRollup.progress * childRollup.cost;
});
// Assign the rolled-up values to this parent node
node.cost = totalCost;
node.progress = totalCost > 0 ? Math.round(weightedProgress / totalCost) : 0;
return { cost: totalCost, progress: node.progress };
}
What does this code do?
This function uses recursion to walk the tree from the bottom up.
- If a node has no children (a leaf), it returns the node's own cost and progress.
- If a node has children, it loops through them, sums their costs, and calculates a cost-weighted average for progress.
- It then assigns the totals directly onto the parent node so the render function can display them.
Why calculate rollups before rendering?
In the WBS methodology, the 100% rule states that all costs and scope at a parent level must equal the sum of the children. By computing rollups first, every node has a valid cost and progress value ready to display when the render function runs.
- Save script.js.
You will not see a visible change yet. The next chunk uses these calculated values to build the tree on screen.
Build the recursive render function
The render function is the core of this project. It takes a single node, creates the HTML elements to represent it, and recursively calls itself for each child node to build the full tree.
- In script.js, add the renderNode function below the calculateRollups function by copying this code:
function renderNode(node) {
// Create the outer wrapper for this node and its children
const nodeEl = document.createElement("div");
nodeEl.classList.add("wbs-node");
// Create the clickable box that displays node info
const boxEl = document.createElement("div");
boxEl.classList.add("wbs-node-box");
// Add the task name
const nameEl = document.createElement("div");
nameEl.classList.add("node-name");
nameEl.textContent = node.name;
// Add the cost display
const costEl = document.createElement("div");
costEl.classList.add("node-cost");
costEl.textContent = "$" + node.cost.toLocaleString();
boxEl.appendChild(nameEl);
boxEl.appendChild(costEl);
if (node.children.length > 0) {
// Add a toggle arrow for nodes with children
const toggleEl = document.createElement("span");
toggleEl.classList.add("toggle-icon");
toggleEl.textContent = "\u25BC";
boxEl.appendChild(toggleEl);
// Create a container for child nodes
const childrenContainer = document.createElement("div");
childrenContainer.classList.add("wbs-children");
// Recursively render each child
node.children.forEach(child => {
childrenContainer.appendChild(renderNode(child));
});
// Toggle children visibility on click
boxEl.addEventListener("click", function () {
childrenContainer.classList.toggle("collapsed");
toggleEl.textContent = childrenContainer.classList.contains("collapsed") ? "\u25B6" : "\u25BC";
});
nodeEl.appendChild(boxEl);
nodeEl.appendChild(childrenContainer);
} else {
nodeEl.appendChild(boxEl);
}
return nodeEl;
}
What does this function do?
For every node in the tree, this function creates a set of nested DOM elements:
- A div.wbs-node wrapper that holds the box and its children.
- A div.wbs-node-box containing the task name and cost.
- A toggle arrow (span.toggle-icon) and a children container (div.wbs-children) for parent nodes.
- A click listener that toggles the collapsed class to hide or show children.
Because the function calls itself for each child, it automatically builds the entire tree no matter how deep the hierarchy goes.
- Save script.js.
You will not see a change in the browser yet. The function is defined but nothing calls it. The next chunk wires everything together.
Seeing a syntax error in your console?
- Check that every opening brace { has a matching closing brace }. This function has nested blocks, so it is easy to miss one.
- Make sure you deleted the old console.log(wbsData); line. Leftover code between functions can cause unexpected errors.
Help me fix a syntax error in my renderNode function
Initialize the tree and verify in the browser
With both functions defined, you need two final lines: one to calculate the rollups and one to kick off rendering into the page.
- At the very bottom of script.js (after the renderNode function's closing brace), add the initialization code:
// Calculate costs and progress for all parent nodes
calculateRollups(wbsData);
// Grab the container element and render the full tree into it
const root = document.getElementById("wbs-root");
root.appendChild(renderNode(wbsData));
What does this code do?
- calculateRollups(wbsData) fills in the null costs on parent nodes so they are ready to display.
- document.getElementById("wbs-root") grabs the empty container div from your HTML.
- renderNode(wbsData) builds the entire tree starting at the root, and appendChild inserts it into the page.
- Save script.js and check your browser.
- You should see unstyled text appearing in a nested structure under the heading. Each task name and a dollar cost should be visible. The layout will look messy because there is no CSS yet.
- Try clicking on a parent node like "Website Redesign" or "Planning". Its children should disappear and the arrow icon should change direction.
Don't see any text on the page?
- Open Chrome DevTools (press Cmd+Option+J on macOS or Ctrl+Shift+J on Windows) and check for red error messages in the Console tab.
- Make sure your index.html still has <div id="wbs-root"></div>. The script looks for that ID to insert the tree.
- Confirm the initialization lines are outside and below both functions, not accidentally inside one of them.
Help me debug why my WBS tree is not appearing
✔️ Awesome, I've got everything!
Great. Double check you have saved script.js and that you can see the tree text in your browser.
ⓧ I'd like to double check the full code
Here is the complete script.js file as it should look at the end of this step:
const wbsData = {
name: "Website Redesign",
cost: null,
progress: null,
children: [
{
name: "Planning",
cost: null,
progress: null,
children: [
{ name: "Requirements Gathering", cost: 3000, progress: 100, children: [] },
{ name: "Stakeholder Interviews", cost: 2000, progress: 80, children: [] },
{ name: "Project Charter", cost: 1000, progress: 60, children: [] }
]
},
{
name: "Design",
cost: null,
progress: null,
children: [
{ name: "Wireframes", cost: 4000, progress: 70, children: [] },
{ name: "Visual Mockups", cost: 5000, progress: 30, children: [] },
{ name: "Prototype", cost: 3000, progress: 10, children: [] }
]
},
{
name: "Development",
cost: null,
progress: null,
children: [
{ name: "Frontend Build", cost: 8000, progress: 20, children: [] },
{ name: "Backend API", cost: 6000, progress: 15, children: [] },
{ name: "Database Setup", cost: 2000, progress: 50, children: [] }
]
},
{
name: "Testing & Launch",
cost: null,
progress: null,
children: [
{ name: "QA Testing", cost: 3000, progress: 0, children: [] },
{ name: "User Acceptance", cost: 2000, progress: 0, children: [] },
{ name: "Deployment", cost: 1000, progress: 0, children: [] }
]
}
]
};
function calculateRollups(node) {
if (node.children.length === 0) {
return { cost: node.cost, progress: node.progress };
}
let totalCost = 0;
let weightedProgress = 0;
node.children.forEach(child => {
const childRollup = calculateRollups(child);
totalCost += childRollup.cost;
weightedProgress += childRollup.progress * childRollup.cost;
});
node.cost = totalCost;
node.progress = totalCost > 0 ? Math.round(weightedProgress / totalCost) : 0;
return { cost: totalCost, progress: node.progress };
}
function renderNode(node) {
const nodeEl = document.createElement("div");
nodeEl.classList.add("wbs-node");
const boxEl = document.createElement("div");
boxEl.classList.add("wbs-node-box");
const nameEl = document.createElement("div");
nameEl.classList.add("node-name");
nameEl.textContent = node.name;
const costEl = document.createElement("div");
costEl.classList.add("node-cost");
costEl.textContent = "$" + node.cost.toLocaleString();
boxEl.appendChild(nameEl);
boxEl.appendChild(costEl);
if (node.children.length > 0) {
const toggleEl = document.createElement("span");
toggleEl.classList.add("toggle-icon");
toggleEl.textContent = "\u25BC";
boxEl.appendChild(toggleEl);
const childrenContainer = document.createElement("div");
childrenContainer.classList.add("wbs-children");
node.children.forEach(child => {
childrenContainer.appendChild(renderNode(child));
});
boxEl.addEventListener("click", function () {
childrenContainer.classList.toggle("collapsed");
toggleEl.textContent = childrenContainer.classList.contains("collapsed") ? "\u25B6" : "\u25BC";
});
nodeEl.appendChild(boxEl);
nodeEl.appendChild(childrenContainer);
} else {
nodeEl.appendChild(boxEl);
}
return nodeEl;
}
calculateRollups(wbsData);
const root = document.getElementById("wbs-root");
root.appendChild(renderNode(wbsData));
Your tree is alive in the browser. It is functional but messy looking. Next up, you will use CSS to transform this raw text into a polished visual tree with proper spacing, boxes, and connecting lines.
Style the WBS Tree with CSS
Your tree is rendering in the browser, but right now it is just a stack of unstyled text. A WBS diagram communicates hierarchy visually through layout and spacing, so the styling is what transforms raw data into a readable project map.
In this step, you will use CSS flexbox to lay out child nodes horizontally, style each node as a bordered card, and draw connecting lines between parent and child nodes using pseudo-elements.
In this step, get ready to:
- Arrange child nodes horizontally with CSS flexbox.
- Style each node as a bordered box showing name and cost.
- Draw connecting lines between nodes using pseudo-elements.
Set up the page foundation styles
Before styling the tree itself, the page needs a clean baseline. A CSS reset removes inconsistent browser defaults, and body styles set the background and spacing.
- Open style.css in VS Code and add the following styles:
/* Remove browser default margins and use border-box sizing */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* Set the page background and spacing */
body {
font-family: Arial, sans-serif;
background: #f0f4f8;
padding: 40px 20px;
min-height: 100vh;
}
/* Center the page heading */
h1 {
text-align: center;
color: #1a202c;
margin-bottom: 40px;
font-size: 1.8rem;
}
What does this code do?
The * selector resets margins, padding, and switches to border-box sizing so that padding never causes unexpected element growth.
The body gets a light blue-grey background (#f0f4f8) with generous padding so the tree never touches the edges of the viewport.
- Save style.css and check your browser.
- You should see the page background change to light grey-blue, and the heading should be centered with space below it.
Page still looks the same?
Make sure the <link rel="stylesheet" href="style.css"> tag in index.html points to the correct filename. The filename is case-sensitive.
Also confirm Live Server is running. If you saved but nothing changed, try right-clicking index.html and selecting Open with Live Server again.
Help me debug why my CSS is not loading in the browser.
Arrange the tree layout with flexbox
A WBS tree flows top-down, with children arranged horizontally beneath their parent. Flexbox handles this perfectly. Each node stacks its content vertically (box on top, children below), and each children container lays its items out in a row.
- Add the following layout styles below the h1 rule in style.css:
/* Center the tree and allow horizontal scrolling */
#wbs-root {
display: flex;
justify-content: center;
overflow-x: auto;
padding: 20px;
}
/* Each node stacks vertically: box on top, children below */
.wbs-node {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
padding-top: 20px;
}
/* Children sit in a horizontal row beneath the parent */
.wbs-children {
display: flex;
gap: 20px;
padding-top: 20px;
position: relative;
}
/* Hide children when a node is collapsed */
.wbs-children.collapsed {
display: none;
}
What does this code do?
The #wbs-root container centers the entire tree and adds horizontal scrolling in case the tree grows wider than the viewport.
Each .wbs-node uses flex-direction: column so the box sits above its children. The .wbs-children container uses the default row direction to spread child nodes horizontally with a 20px gap.
- Save style.css and refresh your browser.
- The tree should now flow top-down with children arranged in a horizontal row beneath each parent. It still looks plain, but the hierarchy is visually clear.
Children still stacking vertically?
Check that .wbs-children has display: flex (not display: block). Flex defaults to row direction, which is what spreads children horizontally.
Help me figure out why my WBS child nodes are not laying out horizontally with flexbox.
Style the node boxes
Each node in a WBS diagram is typically shown as a card or box. You will give each box a white background, a blue border, rounded corners, and a subtle hover effect to show it is clickable.
- Add the following box styles below the .wbs-children.collapsed rule:
/* Style each node as a clickable card */
.wbs-node-box {
background: #ffffff;
border: 2px solid #4a90d9;
border-radius: 8px;
padding: 12px 16px;
min-width: 140px;
text-align: center;
cursor: pointer;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transition: box-shadow 0.2s, transform 0.2s;
position: relative;
}
/* Lift the card on hover to signal interactivity */
.wbs-node-box:hover {
box-shadow: 0 4px 12px rgba(74, 144, 217, 0.3);
transform: translateY(-2px);
}
What does this code do?
The .wbs-node-box class turns each node into a clean white card with a blue border. The cursor: pointer signals that clicking does something.
The transition property animates the hover state smoothly. On hover, the shadow grows and the card lifts 2px upward, giving a tactile "press me" feel.
- Save style.css and refresh your browser.
- Each node should now appear as a white rounded card with a blue border. Hovering over a node should lift it slightly with a blue shadow.
Boxes look right but hover does nothing?
Make sure you included the transition property on .wbs-node-box. Without it, the hover styles apply instantly (no visible animation). Also check that the :hover selector is spelled exactly as .wbs-node-box:hover with no space before the colon.
Help me debug why my CSS hover effect is not working.
- Now add the text styles for the node name, cost, and toggle icon below the hover rule:
/* Bold task name inside the node */
.wbs-node-box .node-name {
font-weight: bold;
font-size: 0.85rem;
color: #1a202c;
margin-bottom: 4px;
}
/* Smaller muted cost label */
.wbs-node-box .node-cost {
font-size: 0.75rem;
color: #4a5568;
}
/* Collapse indicator in the top-right corner */
.wbs-node-box .toggle-icon {
position: absolute;
top: 4px;
right: 6px;
font-size: 0.7rem;
color: #a0aec0;
}
What does this code do?
The .node-name is bold and slightly larger, making it the primary text in each card. The .node-cost sits below in a smaller, muted grey so it does not compete for attention.
The .toggle-icon is positioned absolutely in the top-right corner of the box. This keeps it out of the way of the main content while still being visible.
- Save style.css and refresh your browser.
- Each node box should now show a bold task name on top, a smaller grey cost below it, and a small arrow icon in the top-right corner for nodes with children.
Toggle icon not in the top-right corner?
The toggle icon uses position: absolute, which positions it relative to the nearest positioned ancestor. Make sure .wbs-node-box has position: relative set. Without it, the icon positions itself relative to the page instead.
Help me fix my absolutely positioned toggle icon that is not appearing in the right place.
Add connecting lines between nodes
The final piece that makes a tree diagram readable is the lines connecting parent nodes to their children. CSS pseudo-elements (::before and ::after) can draw these lines without any extra HTML elements.
- Add the following connecting line styles at the bottom of style.css:
/* Vertical line from parent down */
.wbs-node:not(:only-child)::before {
content: "";
position: absolute;
top: 0;
left: 50%;
width: 2px;
height: 20px;
background: #a0aec0;
}
/* Horizontal line connecting siblings */
.wbs-children::before {
content: "";
position: absolute;
top: 0;
left: calc(50% - (100% - 140px) / 2);
right: calc(50% - (100% - 140px) / 2);
height: 2px;
background: #a0aec0;
}
/* Vertical line from horizontal connector to child */
.wbs-children > .wbs-node::before {
content: "";
position: absolute;
top: 0;
left: 50%;
width: 2px;
height: 20px;
background: #a0aec0;
}
/* Hide top connector on root node */
#wbs-root > .wbs-node::before {
display: none;
}
/* Connector from parent box to children area */
.wbs-node > .wbs-children::after {
content: "";
position: absolute;
top: -20px;
left: 50%;
width: 2px;
height: 20px;
background: #a0aec0;
}
How do pseudo-element lines work?
Each ::before or ::after creates an invisible box you can style as a line. By setting a width of 2px and a height of 20px, you get a thin vertical connector. Swapping width and height gives a horizontal line.
The .wbs-children::before draws the horizontal bar that connects all sibling nodes. Each child's ::before draws a vertical line from that bar down into the child box. The root node's connector is hidden with #wbs-root > .wbs-node::before since it has no parent above it.
- Save style.css and refresh your browser.
- You should see grey lines connecting each parent node to its children. A vertical line drops from the parent, a horizontal bar spans across the children, and vertical lines drop from the bar into each child node.
Lines not showing up?
The most common cause is a missing position: relative on the parent element. Pseudo-elements use position: absolute, which requires their nearest ancestor to have relative positioning. Check that both .wbs-node and .wbs-children have position: relative set.
Also make sure you have content: "" on every pseudo-element rule. Without it, the pseudo-element will not render at all.
Help me debug why my CSS pseudo-element connector lines are not appearing in my tree diagram.
✔️ Awesome, I've got everything!
Great. Double check you have saved style.css and that your tree looks like a proper diagram with bordered boxes and connecting lines.
ⓧ I'd like to double check the full code
Here is the complete style.css file. Compare it with yours to find any differences.
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
background: #f0f4f8;
padding: 40px 20px;
min-height: 100vh;
}
h1 {
text-align: center;
color: #1a202c;
margin-bottom: 40px;
font-size: 1.8rem;
}
#wbs-root {
display: flex;
justify-content: center;
overflow-x: auto;
padding: 20px;
}
.wbs-node {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
padding-top: 20px;
}
.wbs-node-box {
background: #ffffff;
border: 2px solid #4a90d9;
border-radius: 8px;
padding: 12px 16px;
min-width: 140px;
text-align: center;
cursor: pointer;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transition: box-shadow 0.2s, transform 0.2s;
position: relative;
}
.wbs-node-box:hover {
box-shadow: 0 4px 12px rgba(74, 144, 217, 0.3);
transform: translateY(-2px);
}
.wbs-node-box .node-name {
font-weight: bold;
font-size: 0.85rem;
color: #1a202c;
margin-bottom: 4px;
}
.wbs-node-box .node-cost {
font-size: 0.75rem;
color: #4a5568;
}
.wbs-node-box .toggle-icon {
position: absolute;
top: 4px;
right: 6px;
font-size: 0.7rem;
color: #a0aec0;
}
.wbs-children {
display: flex;
gap: 20px;
padding-top: 20px;
position: relative;
}
.wbs-children.collapsed {
display: none;
}
/* Vertical line from parent down */
.wbs-node:not(:only-child)::before {
content: "";
position: absolute;
top: 0;
left: 50%;
width: 2px;
height: 20px;
background: #a0aec0;
}
/* Horizontal line connecting siblings */
.wbs-children::before {
content: "";
position: absolute;
top: 0;
left: calc(50% - (100% - 140px) / 2);
right: calc(50% - (100% - 140px) / 2);
height: 2px;
background: #a0aec0;
}
/* Vertical line from horizontal connector to child */
.wbs-children > .wbs-node::before {
content: "";
position: absolute;
top: 0;
left: 50%;
width: 2px;
height: 20px;
background: #a0aec0;
}
/* Hide top connector on root node */
#wbs-root > .wbs-node::before {
display: none;
}
/* Connector from parent box to children area */
.wbs-node > .wbs-children::after {
content: "";
position: absolute;
top: -20px;
left: 50%;
width: 2px;
height: 20px;
background: #a0aec0;
}
Your WBS tree now looks like a proper diagram with clean boxes and connecting lines showing the hierarchy. Next up, you will add collapse and expand interactivity so users can focus on specific parts of the tree.
Add Collapse/Expand Interactivity
Your WBS tree looks great visually, but a real project management tool needs to be interactive. Project managers often collapse sections they're not working on so they can focus on specific phases.
The JavaScript you wrote in previous steps already includes collapse/expand behavior, a toggle icon indicator, and automatic cost rollup. In this step, you'll verify each interactive feature works correctly in the browser and understand how the code achieves it.
In this step, get ready to:
- Test the click event that toggles visibility of child nodes.
- Verify the toggle icon updates between expanded and collapsed states.
- Confirm that cost rollup calculations display correct totals on parent nodes.
Test the collapse/expand click behavior
The collapse/expand feature lets users hide parts of the tree they don't need to see. This is essential for large projects where showing every work package at once creates visual overload.
- In Chrome, click the Planning node box in your WBS tree.
The three child nodes (Requirements Gathering, Stakeholder Interviews, Project Charter) should disappear. Click Planning again and they reappear.
- In script.js, find the click event listener inside the renderNode() function. Locate this code:
boxEl.addEventListener("click", function () {
childrenContainer.classList.toggle("collapsed");
toggleEl.textContent = childrenContainer.classList.contains("collapsed") ? "\u25B6" : "\u25BC";
});
What does this code do?
When you click a parent node's box, classList.toggle("collapsed") adds or removes the collapsed CSS class on the children container.
In your style.css, the rule .wbs-children.collapsed { display: none; } hides the children when this class is present. Toggling the class on and off is what makes nodes appear and disappear.
- Try clicking each of the four phase nodes (Planning, Design, Development, Testing & Launch) to confirm they all collapse and expand independently.
Clicking a node does nothing?
Make sure you're clicking on a parent node (one that has children beneath it). Leaf nodes like Requirements Gathering don't have children to collapse, so nothing happens when you click them.
If parent nodes don't respond either, open Chrome DevTools with Cmd+Option+I (macOS) or Ctrl+Shift+I (Windows) and check the Console tab for JavaScript errors.
Why aren't my WBS nodes responding to clicks?
Verify the toggle icon indicator
A good UI gives users a visual hint about what's clickable. The small arrow icon in the top-right corner of each parent node tells users whether the node is expanded or collapsed.
- Look at the top-right corner of the Website Redesign root node. You should see a small downward-pointing arrow (▼).
- Click the Website Redesign node. The arrow changes to a right-pointing triangle (▶), indicating the children are now hidden.
- Click it again. The arrow returns to the downward triangle (▼).
- In script.js, find the toggle icon creation code inside renderNode():
const toggleEl = document.createElement("span");
toggleEl.classList.add("toggle-icon");
toggleEl.textContent = "\u25BC";
boxEl.appendChild(toggleEl);
How does the icon switch?
The icon starts as \u25BC (a downward triangle, meaning "expanded"). Inside the click handler, a ternary expression checks whether the collapsed class is present.
If collapsed, the icon switches to \u25B6 (a right-pointing triangle). If expanded, it returns to \u25BC. This toggle only exists on parent nodes because it's inside the if (node.children.length > 0) block.
Confirm cost rollup calculations
In a Work Breakdown Structure, costs always roll up from the bottom. Leaf nodes (individual work packages) have known costs. Parent nodes show the sum of all their children's costs.
- Look at the Planning node. It should display $6,000 (the sum of $3,000 + $2,000 + $1,000 from its three children).
- Check the Website Redesign root node. It should display $40,000 (the total of all 12 leaf-node costs combined).
- In script.js, find the calculateRollups() function near the middle of the file:
function calculateRollups(node) {
if (node.children.length === 0) {
return { cost: node.cost, progress: node.progress };
}
let totalCost = 0;
let weightedProgress = 0;
node.children.forEach(child => {
const childRollup = calculateRollups(child);
totalCost += childRollup.cost;
weightedProgress += childRollup.progress * childRollup.cost;
});
node.cost = totalCost;
node.progress = totalCost > 0 ? Math.round(weightedProgress / totalCost) : 0;
return { cost: totalCost, progress: node.progress };
}
What does calculateRollups do?
This function uses recursion to walk the tree from the bottom up. When it hits a leaf node (no children), it returns that node's cost and progress directly.
For parent nodes, it sums up all children's costs and calculates a weighted average for progress. Weighting by cost means expensive tasks count more toward overall completion. A $8,000 task at 20% progress contributes more to the parent's average than a $1,000 task at 60%.
The function is called once before rendering with calculateRollups(wbsData), so every parent node has its totals ready before the tree is drawn.
Why weight progress by cost?
In real project management, a simple average would be misleading. If three tasks cost $1,000, $1,000, and $8,000, and only the two cheap tasks are done, a simple average says 67% complete. But the weighted average says 20% complete, because the bulk of the budget is still unfinished. The weighted approach gives a more honest picture of project health.
- Collapse the Website Redesign root node by clicking it. Notice the $40,000 total stays visible on the collapsed node, giving you a quick summary without needing to see every child.
- Click it again to expand and verify all four phases are back.
Seeing wrong cost values?
If parent nodes show $0 or null, make sure the line calculateRollups(wbsData); appears BEFORE root.appendChild(renderNode(wbsData)); at the bottom of your file. The rollup must finish computing before the tree renders.
My WBS tree is showing wrong cost values on parent nodes.
✔️ Awesome, I've got everything!
Great. Double check you've saved script.js and that all three interactive features work: collapse/expand on click, toggle icon switching, and correct cost rollups.
ⓧ I'd like to double check the full code
Here is the complete script.js file. Compare your code against this:
const wbsData = {
name: "Website Redesign",
cost: null,
progress: null,
children: [
{
name: "Planning",
cost: null,
progress: null,
children: [
{ name: "Requirements Gathering", cost: 3000, progress: 100, children: [] },
{ name: "Stakeholder Interviews", cost: 2000, progress: 80, children: [] },
{ name: "Project Charter", cost: 1000, progress: 60, children: [] }
]
},
{
name: "Design",
cost: null,
progress: null,
children: [
{ name: "Wireframes", cost: 4000, progress: 70, children: [] },
{ name: "Visual Mockups", cost: 5000, progress: 30, children: [] },
{ name: "Prototype", cost: 3000, progress: 10, children: [] }
]
},
{
name: "Development",
cost: null,
progress: null,
children: [
{ name: "Frontend Build", cost: 8000, progress: 20, children: [] },
{ name: "Backend API", cost: 6000, progress: 15, children: [] },
{ name: "Database Setup", cost: 2000, progress: 50, children: [] }
]
},
{
name: "Testing & Launch",
cost: null,
progress: null,
children: [
{ name: "QA Testing", cost: 3000, progress: 0, children: [] },
{ name: "User Acceptance", cost: 2000, progress: 0, children: [] },
{ name: "Deployment", cost: 1000, progress: 0, children: [] }
]
}
]
};
function calculateRollups(node) {
if (node.children.length === 0) {
return { cost: node.cost, progress: node.progress };
}
let totalCost = 0;
let weightedProgress = 0;
node.children.forEach(child => {
const childRollup = calculateRollups(child);
totalCost += childRollup.cost;
weightedProgress += childRollup.progress * childRollup.cost;
});
node.cost = totalCost;
node.progress = totalCost > 0 ? Math.round(weightedProgress / totalCost) : 0;
return { cost: totalCost, progress: node.progress };
}
function renderNode(node) {
const nodeEl = document.createElement("div");
nodeEl.classList.add("wbs-node");
const boxEl = document.createElement("div");
boxEl.classList.add("wbs-node-box");
const nameEl = document.createElement("div");
nameEl.classList.add("node-name");
nameEl.textContent = node.name;
const costEl = document.createElement("div");
costEl.classList.add("node-cost");
costEl.textContent = "$" + node.cost.toLocaleString();
boxEl.appendChild(nameEl);
boxEl.appendChild(costEl);
if (node.children.length > 0) {
const toggleEl = document.createElement("span");
toggleEl.classList.add("toggle-icon");
toggleEl.textContent = "\u25BC";
boxEl.appendChild(toggleEl);
const childrenContainer = document.createElement("div");
childrenContainer.classList.add("wbs-children");
node.children.forEach(child => {
childrenContainer.appendChild(renderNode(child));
});
boxEl.addEventListener("click", function () {
childrenContainer.classList.toggle("collapsed");
toggleEl.textContent = childrenContainer.classList.contains("collapsed") ? "\u25B6" : "\u25BC";
});
nodeEl.appendChild(boxEl);
nodeEl.appendChild(childrenContainer);
} else {
nodeEl.appendChild(boxEl);
}
return nodeEl;
}
calculateRollups(wbsData);
const root = document.getElementById("wbs-root");
root.appendChild(renderNode(wbsData));
Your WBS viewer is now fully interactive. You've built a complete project management visualization tool that collapses, expands, and rolls up costs automatically. Next up, the Secret Mission will challenge you to add progress bars and a live summary panel.
Secret mission
Add Progress Bars and a Summary Panel
Your WBS tree shows structure and cost, but what about progress? In this bonus challenge, add a color-coded progress bar inside every node and a live summary panel at the top showing total cost and overall completion. The result: a dashboard-ready WBS viewer that tells you at a glance how a project is tracking.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.
Resources you used:
- The wbs-viewer project folder on your Desktop (contains index.html, style.css, and script.js).
- The Live Server VS Code extension.
- VS Code (if you installed it for this project).
Keep everything running
No action needed. Choose this if you plan to keep experimenting with the WBS viewer or want to reuse the code as a template for other projects.
- Your wbs-viewer folder stays on your Desktop, ready to open anytime.
- VS Code and the Live Server extension remain installed for future projects.
Pause - I'll come back to this later
Since this is a local project with no running processes, there is nothing to pause. Your files sit on disk without consuming memory or CPU.
- Close the browser tab showing your WBS viewer.
- Close VS Code.
- When you want to return, open the wbs-viewer folder in VS Code and launch Live Server again.
Delete - I don't want to use this again
Remove all project files and extensions you installed during this project.
- Close VS Code and any browser tabs showing the WBS viewer.
- Delete the project folder by running these commands in your terminal:
macOS
cd ~/Desktop
rm -rf wbs-viewer
Windows
cd ~\Desktop
rmdir /s /q wbs-viewer
- To remove the Live Server extension, open VS Code, click the Extensions icon in the left sidebar, search for Live Server, and click Uninstall.
- To uninstall VS Code itself, follow the official uninstall guide at https://code.visualstudio.com/docs/setup/uninstall.
Nice Work!
Nice Work!
You built an interactive Work Breakdown Structure viewer from scratch using pure HTML, CSS, and JavaScript. Your browser now renders a complete project hierarchy with costs, progress tracking, and collapse/expand functionality.
You've learned how to:
- Built a hierarchical data model in JavaScript representing a project broken into phases and work packages, with automatic cost rollup calculations from leaf nodes to parent nodes.
- Rendered a visual tree layout using a recursive renderNode() function and CSS flexbox, with connecting lines between nodes using CSS pseudo-elements.
- Implemented collapse/expand interactivity with DOM event handling, learning the WBS methodology of decomposing work into deliverable-oriented packages with cost and progress tracking.
- Added color-coded progress bars and a live summary panel with automatic cost and weighted progress calculations.
Ready to quiz yourself?