Build a Personal Dashboard with JS
Build a browser dashboard with a live clock, quotes API, and to-do list.
Introduction
30 Second Summary
Every time you open your browser, you probably check the time, glance at a to-do list, or look for a bit of inspiration. What if one page handled all of that for you the moment it loaded?
In this project, you will build a personal dashboard web app using JavaScript. It displays a live clock, fetches inspirational quotes from the internet, and saves your to-do list between browser sessions.
What You'll Build
A single-page personal dashboard that runs entirely in your browser, built with HTML, CSS, and vanilla JavaScript.
By the end of this project, you'll have:
- A live clock that updates every second, showing the current time and date.
- A quote fetcher that pulls a random inspirational quote from a real API every time you load the page.
- A to-do list with add and delete functionality, persisted across page refreshes using localStorage.
- Secret Mission: A smart greeting that changes based on the time of day and a focus task feature that highlights your most important task.
Are there any prerequisites?
None. This project includes setup instructions for every tool you need. All you need is a computer with a web browser.
Set Up Your Project
Before you can build your dashboard's features, you need a project folder and the files your browser will read.
In this step, you'll create the project structure and write the HTML boilerplate that connects your CSS and JavaScript files together.
In this step, get ready to:
- Create a project folder with three files on your Desktop.
- Write the HTML boilerplate that links your stylesheet and script.
- Launch the page in your browser with Live Server.
Create your project folder and files
Every web project starts with a folder to hold your files. You'll create one on your Desktop so it's easy to find.
- Open a new terminal in VS Code by clicking Terminal in the top menu bar.
- Click New Terminal.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir personal-dashboard
cd personal-dashboard
- Create the three files your project needs by running this command:
touch 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.
Don't see the three files?
Make sure you ran cd personal-dashboard before running touch. If you created the files in the wrong folder, delete them and start again from the mkdir step.
Help me figure out what went wrong.
Now you need to open this folder in VS Code so the editor can see all your files.
- Click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the personal-dashboard folder, and click Open.
Why open the folder and not just the files?
VS Code works best when it has a folder open as a workspace. The Live Server extension also requires a folder to be open before it can serve your pages.
You should see index.html, style.css, and script.js listed in the Explorer sidebar on the left.
Write the HTML boilerplate
Your index.html file is the entry point for your dashboard. It connects your CSS for styling and your JavaScript for interactivity.
- Click index.html in the Explorer sidebar to open it in the editor.
- Add the following HTML boilerplate:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Personal Dashboard</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="dashboard">
</div>
<script src="script.js"></script>
</body>
</html>
What does this code do?
This is the skeleton of your web page. The <link> tag pulls in your stylesheet so the browser knows how to style the page. The <script> tag at the bottom loads your JavaScript after the page structure is ready.
The empty <div class="dashboard"> is the container where all your dashboard sections will live. You'll fill it in over the next few steps.
- Save index.html by pressing Cmd+S (macOS) or Ctrl+S (Windows).
Now launch the page in your browser to confirm everything is wired up.
- Right-click index.html in the Explorer sidebar.
- Select Open with Live Server.
A browser tab should open showing a blank page. This is correct. Your HTML has no visible content yet, but the page is being served locally.
What is Live Server?
Live Server runs a local web server on your computer and opens your page in the browser. Every time you save a file, it automatically refreshes the browser so you can see your changes instantly.
Don't see a browser tab open?
Make sure you opened the personal-dashboard folder in VS Code (not just a single file). Live Server requires a folder workspace to be open.
If the right-click option is missing, try clicking Go Live in the blue status bar at the very bottom of VS Code instead.
Help me troubleshoot Live Server.
✔️ Awesome, I've got everything!
Great. Double check you've saved index.html and that Live Server is running (you should see a port number in the VS Code status bar).
ⓧ I'd like to double check the full code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Personal Dashboard</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="dashboard">
</div>
<script src="script.js"></script>
</body>
</html>
Your project skeleton is in place. Next up, you'll bring it to life by building a clock that ticks every second.
Build the Live Clock and Date
Your project files are ready and Live Server is running. Now it is time to build the first feature of your dashboard: a live clock that shows the current time and date.
JavaScript can read the current time from your computer and update the page every second. You will use this to create a clock that ticks in real time, just like the one on your phone.
In this step, get ready to:
- Add HTML elements to display the time and date.
- Write a JavaScript function that reads the current time and updates the page.
- Set the clock to update automatically every second.
Add the clock HTML
Your clock needs two elements on the page: one for the time and one for the date. You will give each element an ID so that JavaScript can find and update them.
- In VS Code, open index.html.
- Find the <div class="dashboard"> element. Add the following code inside it:
<section class="clock-section">
<h1 id="time">00:00:00</h1>
<p id="date">Loading date...</p>
</section>
What does this code do?
The <section> groups your clock elements together with a class name for styling later.
The <h1 id="time"> element displays the time. The <p id="date"> element displays the date. Both have placeholder text that JavaScript will replace with real values.
- Save index.html.
- Check your browser. You should see 00:00:00 and Loading date... displayed on the page.
Don't see the placeholder text?
- Make sure you pasted the code inside the <div class="dashboard"> tags, not outside them.
- Check that Live Server is still running. You should see a port number (like 5500) in the VS Code status bar at the bottom.
Why isn't my HTML showing up in the browser?
✔️ Awesome, I've got everything!
Great. Make sure you have saved index.html before moving on.
ⓧ I'd like to double check the full code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Personal Dashboard</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="dashboard">
<section class="clock-section">
<h1 id="time">00:00:00</h1>
<p id="date">Loading date...</p>
</section>
</div>
<script src="script.js"></script>
</body>
</html>
Write the clock function
JavaScript's Date object gives you the current time from the learner's computer. You will create a function that reads the hours, minutes, and seconds, formats them with leading zeros, and writes them to the page.
- Open script.js and add the following function:
// --- Clock ---
function updateClock() {
const now = new Date();
let hours = now.getHours();
let minutes = now.getMinutes();
let seconds = now.getSeconds();
hours = hours.toString().padStart(2, '0');
minutes = minutes.toString().padStart(2, '0');
seconds = seconds.toString().padStart(2, '0');
document.getElementById('time').textContent = `${hours}:${minutes}:${seconds}`;
const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
document.getElementById('date').textContent = now.toLocaleDateString('en-US', options);
}
What does this function do?
- new Date() creates an object holding the current date and time from your computer.
- getHours(), getMinutes(), and getSeconds() pull out the individual time values as numbers.
- padStart(2, '0') adds a leading zero to single-digit numbers so the clock always shows two digits (e.g. 09 instead of 9).
- document.getElementById('time').textContent finds the <h1> element by its ID and replaces its text with the formatted time.
- toLocaleDateString('en-US', options) formats the date into a readable string like "Monday, June 9, 2026".
You will not see a change in the browser yet. The function is defined but nothing is calling it. The next substep activates it.
Make the clock tick every second
A clock that only shows the time once is not very useful. You need it to update every second. JavaScript's setInterval function repeats any function on a timer you choose.
- In script.js, add these two lines below the closing } of your updateClock function:
updateClock();
setInterval(updateClock, 1000);
Why call updateClock() before setInterval?
setInterval waits 1000 milliseconds (one second) before running the function for the first time. Without the direct updateClock() call, the clock would show 00:00:00 for a full second before jumping to the real time.
Calling it once immediately fills in the correct time right away. Then setInterval keeps it updating every second after that.
- Save script.js.
- Check your browser. You should see the current time displayed and updating every second. Below it, today's date appears in a format like "Monday, June 9, 2026".
Clock not showing or not updating?
- Make sure the two new lines (updateClock() and setInterval(updateClock, 1000)) are outside and below the function's closing } brace. If they are inside the function, they will never run.
- Open the browser console (right-click the page, click Inspect, then the Console tab). Look for red error messages. A typo in an ID name like tme instead of time will cause an error here.
- Confirm that your index.html has <script src="script.js"></script> before the closing </body> tag.
Help me debug why my clock is not working.
✔️ Awesome, I've got everything!
Great. Make sure you have saved script.js before moving on.
ⓧ I'd like to double check the full code
// --- Clock ---
function updateClock() {
const now = new Date();
let hours = now.getHours();
let minutes = now.getMinutes();
let seconds = now.getSeconds();
hours = hours.toString().padStart(2, '0');
minutes = minutes.toString().padStart(2, '0');
seconds = seconds.toString().padStart(2, '0');
document.getElementById('time').textContent = `${hours}:${minutes}:${seconds}`;
const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
document.getElementById('date').textContent = now.toLocaleDateString('en-US', options);
}
updateClock();
setInterval(updateClock, 1000);
Your clock is ticking. Next up, you will fetch a random inspirational quote from the internet and display it on your dashboard.
Fetch and Display a Random Quote
Your clock is ticking away nicely. Now it is time to pull in content from the outside world.
In this step, you will use JavaScript's fetch function to call a free quotes API and display a random inspirational quote on your dashboard.
In this step, get ready to:
- Add a quote section to your HTML.
- Write an async function that fetches a quote from an API.
- Wire up a button to load a new quote on click.
Add the quote section HTML
Your dashboard needs a place to display the quote text, the author's name, and a button to request a fresh quote.
- In VS Code, open index.html.
- Find the closing </section> tag of your clock section.
- Add the following quote section below it (but still inside the .dashboard div):
<section class="quote-section">
<p id="quote-text">Loading quote...</p>
<p id="quote-author"></p>
<button id="new-quote-btn">New Quote</button>
</section>
What does this code do?
- The p#quote-text paragraph holds the quote itself. Its placeholder text says "Loading quote..." until the JavaScript replaces it.
- The p#quote-author paragraph will display who said the quote.
- The button#new-quote-btn lets the user request a different quote on demand.
- Save index.html.
- Check your browser. You should see the text "Loading quote..." and a "New Quote" button below the clock.
Don't see the new section?
- Make sure the new <section> is inside the <div class="dashboard"> but after the closing </section> of the clock section.
- Confirm you saved the file. Live Server only reloads when the file is saved.
Ask for help: Why isn't my quote section showing up in the browser?
Write the fetch function
JavaScript's fetch function lets your code request data from a server. You will call a free quotes API that returns a random quote every time you hit it.
- Open script.js.
- At the bottom of the file, below the setInterval(updateClock, 1000); line, add the following function:
// --- Quote ---
async function fetchQuote() {
try {
const response = await fetch('https://quotes.domi.my.id');
const data = await response.json();
document.getElementById('quote-text').textContent = `"${data.quote}"`;
document.getElementById('quote-author').textContent = `- ${data.author}`;
} catch (error) {
document.getElementById('quote-text').textContent = 'Could not load quote. Try again!';
document.getElementById('quote-author').textContent = '';
}
}
What does this code do?
- async marks the function so it can use await to pause until the server responds.
- fetch('https://quotes.domi.my.id') sends a request to the quotes API. The API returns a JSON object with quote and author fields.
- response.json() converts the raw response into a JavaScript object you can read.
- The try/catch block handles network failures gracefully. If the request fails, users see a friendly fallback message instead of a broken page.
- Save script.js.
- You will not see a quote appear yet. The function exists but nothing calls it. The next chunk wires it up.
Call the function and add the button listener
A function does nothing until something invokes it. You need to call fetchQuote() once on page load and connect the button so clicking it fetches a fresh quote.
- In script.js, add these two lines directly below your fetchQuote function:
fetchQuote();
document.getElementById('new-quote-btn').addEventListener('click', fetchQuote);
What does this code do?
- fetchQuote() runs the function immediately when the page loads, so users see a quote right away.
- addEventListener('click', fetchQuote) tells the browser to run fetchQuote again every time the user clicks the "New Quote" button.
- Save script.js.
- Check your browser. A random quote with its author should appear below the clock. Click the New Quote button to see a different one appear.
No quote appearing?
- Open your browser's developer console (Cmd+Option+J on macOS or Ctrl+Shift+J on Windows). Look for red error messages.
- If you see a network error, confirm your internet connection is active. The API requires an internet connection to respond.
- Check that the IDs in your HTML (quote-text, quote-author, new-quote-btn) match the IDs in your JavaScript exactly.
Ask for help: My quote section shows 'Loading quote...' and never changes. How do I debug this?
✔️ Awesome, I've got everything!
Great. Double check you have saved both index.html and script.js.
ⓧ I'd like to double check the full code
Here is the full code for both files at this point.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Personal Dashboard</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="dashboard">
<section class="clock-section">
<h1 id="time">00:00:00</h1>
<p id="date">Loading date...</p>
</section>
<section class="quote-section">
<p id="quote-text">Loading quote...</p>
<p id="quote-author"></p>
<button id="new-quote-btn">New Quote</button>
</section>
</div>
<script src="script.js"></script>
</body>
</html>
// --- Clock ---
function updateClock() {
const now = new Date();
let hours = now.getHours();
let minutes = now.getMinutes();
let seconds = now.getSeconds();
hours = hours.toString().padStart(2, '0');
minutes = minutes.toString().padStart(2, '0');
seconds = seconds.toString().padStart(2, '0');
document.getElementById('time').textContent = `${hours}:${minutes}:${seconds}`;
const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
document.getElementById('date').textContent = now.toLocaleDateString('en-US', options);
}
updateClock();
setInterval(updateClock, 1000);
// --- Quote ---
async function fetchQuote() {
try {
const response = await fetch('https://quotes.domi.my.id');
const data = await response.json();
document.getElementById('quote-text').textContent = `"${data.quote}"`;
document.getElementById('quote-author').textContent = `- ${data.author}`;
} catch (error) {
document.getElementById('quote-text').textContent = 'Could not load quote. Try again!';
document.getElementById('quote-author').textContent = '';
}
}
fetchQuote();
document.getElementById('new-quote-btn').addEventListener('click', fetchQuote);
Your dashboard now pulls live data from the internet. Next up, you will build a to-do list that remembers your tasks even after you close the browser.
Build the To-Do List with localStorage
Your dashboard shows the time and a fresh quote on every visit. Now it is time to make it truly useful by adding a to-do list.
The catch with web apps is that any data you create disappears when you refresh the page. To solve this, you will use localStorage to save tasks directly inside the browser so they persist between sessions.
In this step, get ready to:
- Add a to-do list section to your HTML with a form and task list.
- Write JavaScript functions that read, save, and render tasks.
- Persist tasks across page refreshes using localStorage.
Add the to-do HTML
Your to-do list needs three things: a heading, a form for entering new tasks, and an empty list that JavaScript will populate.
- In index.html, find the closing </section> tag of the quote section.
- Below it (but still inside the <div class="dashboard">), add the following to-do section:
<section class="todo-section">
<h2>To-Do List</h2>
<form id="todo-form">
<input type="text" id="todo-input" placeholder="Add a new task..." required>
<button type="submit">Add</button>
</form>
<ul id="todo-list"></ul>
</section>
What does this code do?
The <form> wraps the input and button together. When the learner presses Enter or clicks Add, the browser fires a submit event that your JavaScript will intercept.
The required attribute prevents the form from submitting if the input is empty. The <ul id="todo-list"> starts empty because JavaScript will fill it with list items.
- Save index.html.
- Check your browser. You should see a "To-Do List" heading, a text input field, and an Add button below your quote section.
Don't see the to-do section?
Make sure the new <section> sits inside the <div class="dashboard"> and before the closing </div>. If it is placed after the </div>, the dashboard layout will not include it.
help me find where my to-do section HTML should go in index.html
Write the storage helpers and renderer
Before you can add or delete tasks, you need a few helper functions. These handle reading from and writing to localStorage, plus building the visible list from stored data.
- In script.js, scroll to the bottom of the file (below the fetchQuote event listener line).
- Add the following code to grab your DOM elements and create the storage helpers:
// --- To-Do List ---
const todoForm = document.getElementById('todo-form');
const todoInput = document.getElementById('todo-input');
const todoList = document.getElementById('todo-list');
function getTodos() {
return JSON.parse(localStorage.getItem('todos') || '[]');
}
function saveTodos(todos) {
localStorage.setItem('todos', JSON.stringify(todos));
}
What does this code do?
The three const lines grab references to your form, input field, and list so you can use them throughout the script without looking them up every time.
The getTodos() function reads the 'todos' key from localStorage. Since localStorage only stores strings, JSON.parse() converts the stored string back into a JavaScript array. If nothing is stored yet, it falls back to an empty array '[]'.
The saveTodos() function does the reverse. It takes your array of tasks and converts it to a string with JSON.stringify() before saving it to localStorage.
- Save script.js.
You will not see any change in the browser yet. These helper functions are called by the code you write next.
- Below the saveTodos function, add the renderTodos function that builds the visible task list:
function renderTodos() {
const todos = getTodos();
todoList.innerHTML = '';
todos.forEach(function (task, index) {
const li = document.createElement('li');
li.textContent = task;
const deleteBtn = document.createElement('button');
deleteBtn.textContent = 'x';
deleteBtn.classList.add('delete-btn');
deleteBtn.addEventListener('click', function () {
deleteTodo(index);
});
li.appendChild(deleteBtn);
todoList.appendChild(li);
});
}
What does this code do?
Every time renderTodos() runs, it wipes the current list with todoList.innerHTML = '' and rebuilds it from scratch using whatever is in localStorage. This "clear and rebuild" pattern keeps the display perfectly in sync with stored data.
For each task, it creates an <li> element and a delete button. The delete button gets a click listener that calls deleteTodo(index) (you will write that function next).
- Save script.js.
You will not see a change yet. The renderer is ready but nothing is calling it.
Seeing an error in the console?
Open your browser's Developer Tools (Cmd+Option+I on macOS or Ctrl+Shift+I on Windows) and check the Console tab. If you see a reference error, make sure the todo-section HTML from the previous substep is saved. The getElementById calls at the top need those elements to exist.
help me debug a console error in my to-do list JavaScript
Add, delete, and wire up the form
Now you will write the functions that actually modify the task list, and connect everything to the form so it responds to user input.
- Below the renderTodos function in script.js, add the addTodo and deleteTodo functions:
function addTodo(task) {
const todos = getTodos();
todos.push(task);
saveTodos(todos);
renderTodos();
}
function deleteTodo(index) {
const todos = getTodos();
todos.splice(index, 1);
saveTodos(todos);
renderTodos();
}
What does this code do?
Both functions follow the same pattern: get the current array from localStorage, modify it, save it back, and re-render the list.
The addTodo function uses .push() to add the new task to the end of the array. The deleteTodo function uses .splice(index, 1) to remove exactly one item at the given position.
- Below the deleteTodo function, add the form event listener and the initial renderTodos() call:
todoForm.addEventListener('submit', function (event) {
event.preventDefault();
const task = todoInput.value.trim();
if (task) {
addTodo(task);
todoInput.value = '';
}
});
renderTodos();
What does this code do?
The submit event listener intercepts the form. Normally, submitting a form causes the browser to reload the page. Calling event.preventDefault() stops that reload so your JavaScript can handle the input instead.
It grabs the text from the input, trims whitespace, and passes it to addTodo() if the value is not empty. Then it clears the input field so the learner can type a new task right away.
The standalone renderTodos() call at the bottom runs once when the page first loads. This is what pulls any previously saved tasks out of localStorage and displays them immediately.
- Save script.js.
- Switch to your browser. Type a task (like Learn JavaScript) into the input field and press Enter or click Add.
- Your task should appear in the list below the form with a small x button next to it.
- Refresh the page. Your task should still be there.
- Click the x button next to a task and refresh again. It should be gone.
Tasks disappearing on refresh?
Make sure you have both the saveTodos() call inside addTodo and the standalone renderTodos() call at the very bottom of your script. Without saveTodos(), tasks are never written to localStorage. Without the final renderTodos(), saved tasks are never read back on page load.
Also confirm that Live Server is serving over http://localhost (check your browser address bar). Opening the file directly with a file:// URL can cause localStorage to behave unpredictably.
help me debug why my to-do tasks are not persisting after page refresh
✔️ Awesome, I've got everything!
Great. Double check you have saved both index.html and script.js.
ⓧ I'd like to double check the full code
Here is the full index.html at this point:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Personal Dashboard</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="dashboard">
<section class="clock-section">
<h1 id="time">00:00:00</h1>
<p id="date">Loading date...</p>
</section>
<section class="quote-section">
<p id="quote-text">Loading quote...</p>
<p id="quote-author"></p>
<button id="new-quote-btn">New Quote</button>
</section>
<section class="todo-section">
<h2>To-Do List</h2>
<form id="todo-form">
<input type="text" id="todo-input" placeholder="Add a new task..." required>
<button type="submit">Add</button>
</form>
<ul id="todo-list"></ul>
</section>
</div>
<script src="script.js"></script>
</body>
</html>
Here is the full script.js at this point:
// --- Clock ---
function updateClock() {
const now = new Date();
let hours = now.getHours();
let minutes = now.getMinutes();
let seconds = now.getSeconds();
hours = hours.toString().padStart(2, '0');
minutes = minutes.toString().padStart(2, '0');
seconds = seconds.toString().padStart(2, '0');
document.getElementById('time').textContent = `${hours}:${minutes}:${seconds}`;
const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
document.getElementById('date').textContent = now.toLocaleDateString('en-US', options);
}
updateClock();
setInterval(updateClock, 1000);
// --- Quote ---
async function fetchQuote() {
try {
const response = await fetch('https://quotes.domi.my.id');
const data = await response.json();
document.getElementById('quote-text').textContent = `"${data.quote}"`;
document.getElementById('quote-author').textContent = `- ${data.author}`;
} catch (error) {
document.getElementById('quote-text').textContent = 'Could not load quote. Try again!';
document.getElementById('quote-author').textContent = '';
}
}
fetchQuote();
document.getElementById('new-quote-btn').addEventListener('click', fetchQuote);
// --- To-Do List ---
const todoForm = document.getElementById('todo-form');
const todoInput = document.getElementById('todo-input');
const todoList = document.getElementById('todo-list');
function getTodos() {
return JSON.parse(localStorage.getItem('todos') || '[]');
}
function saveTodos(todos) {
localStorage.setItem('todos', JSON.stringify(todos));
}
function renderTodos() {
const todos = getTodos();
todoList.innerHTML = '';
todos.forEach(function (task, index) {
const li = document.createElement('li');
li.textContent = task;
const deleteBtn = document.createElement('button');
deleteBtn.textContent = 'x';
deleteBtn.classList.add('delete-btn');
deleteBtn.addEventListener('click', function () {
deleteTodo(index);
});
li.appendChild(deleteBtn);
todoList.appendChild(li);
});
}
function addTodo(task) {
const todos = getTodos();
todos.push(task);
saveTodos(todos);
renderTodos();
}
function deleteTodo(index) {
const todos = getTodos();
todos.splice(index, 1);
saveTodos(todos);
renderTodos();
}
todoForm.addEventListener('submit', function (event) {
event.preventDefault();
const task = todoInput.value.trim();
if (task) {
addTodo(task);
todoInput.value = '';
}
});
renderTodos();
Your dashboard now has a fully working to-do list that remembers your tasks between visits. Next up, you will style the entire dashboard so it looks polished and professional.
Style Your Dashboard
Your dashboard is fully functional. The clock ticks, quotes load from an API, and your to-do list saves tasks across page refreshes.
Right now everything looks like raw browser defaults. In this step, you will transform your dashboard into a polished, dark-themed interface using CSS. You will use a gradient background, glass-effect cards, and responsive units so it looks great on any screen.
In this step, get ready to:
- Add a CSS reset and dark gradient background with centered layout.
- Style the clock, quote, and to-do sections with clean typography and spacing.
- Use responsive units so your dashboard adapts to different screen sizes.
Set up the base styles
Every browser applies its own default margins and padding to elements. A CSS reset removes these so your layout starts from a clean slate.
- In style.css, add the CSS reset, body styles, and dashboard container by pasting this code:
/* Remove default browser margins and padding */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* Center the dashboard on a dark gradient background */
body {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #0f0c29, #302b63, #24243e);
font-family: 'Segoe UI', sans-serif;
color: #ffffff;
}
/* Constrain the dashboard width for readability */
.dashboard {
width: 90%;
max-width: 600px;
padding: 2rem;
}
What does this code do?
The * selector targets every element on the page and strips away default spacing. box-sizing: border-box makes padding count inside element widths instead of adding to them.
The body uses flexbox to center the dashboard both horizontally and vertically. The linear-gradient creates a smooth transition between three dark purple colours.
The .dashboard container caps content at 600px wide. On smaller screens it uses 90% of the viewport width instead.
- Save style.css.
- Check your browser. You should see a dark purple gradient background with all your dashboard text in white, centered on the page.
Still seeing a white background?
Make sure your index.html has <link rel="stylesheet" href="style.css"> in the <head> section. If the link is missing, the browser cannot find your stylesheet.
Also confirm that style.css is in the same folder as index.html.
Help me debug my CSS not loading.
Style each section
With the base layout in place, you will now style each of the three dashboard sections individually. The clock gets large, bold text. The quote section gets a glass-effect card. The to-do list gets clean hover interactions.
- Add the clock section styles below your existing code in style.css:
/* Clock section - large centered time display */
.clock-section {
text-align: center;
margin-bottom: 2rem;
}
#time {
font-size: clamp(2.5rem, 8vw, 4rem);
font-weight: 700;
letter-spacing: 4px;
font-family: 'Segoe UI', monospace;
}
#date {
font-size: 1.1rem;
color: rgba(255, 255, 255, 0.6);
margin-top: 0.5rem;
}
What does this code do?
The clamp() function sets a responsive font size. The time text will be at least 2.5rem, at most 4rem, and scales with 8vw (8% of viewport width) in between.
The date uses a semi-transparent white colour (rgba with 0.6 opacity) so it appears muted compared to the clock.
- Save style.css and check your browser.
- The clock should now appear large and bold with a monospace font. The date sits below in a lighter, muted tone.
- Next, add the quote section styles below the clock styles:
/* Quote section - glass-effect card */
.quote-section {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 2rem;
text-align: center;
}
#quote-text {
font-style: italic;
font-size: 1.1rem;
line-height: 1.6;
margin-bottom: 0.75rem;
}
#quote-author {
font-size: 0.9rem;
color: rgba(255, 255, 255, 0.6);
margin-bottom: 1rem;
}
/* New Quote button with hover effect */
#new-quote-btn {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
color: #ffffff;
padding: 0.5rem 1.2rem;
border-radius: 6px;
cursor: pointer;
font-size: 0.85rem;
transition: background 0.2s;
}
#new-quote-btn:hover {
background: rgba(255, 255, 255, 0.2);
}
What does this code do?
The .quote-section uses a near-transparent white background with a subtle border. This creates a frosted-glass effect against the gradient.
The transition: background 0.2s on the button makes the hover effect animate smoothly instead of snapping instantly.
- Save style.css and check your browser.
- The quote section should now sit inside a subtle glass-like card with rounded corners. Hover over the New Quote button to see it brighten.
- Finally, add the to-do section styles below the quote styles:
/* To-do section heading */
.todo-section h2 {
margin-bottom: 1rem;
font-size: 1.3rem;
}
/* Form layout - input and button side by side */
#todo-form {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
}
#todo-input {
flex: 1;
padding: 0.6rem 1rem;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 6px;
background: rgba(255, 255, 255, 0.05);
color: #ffffff;
font-size: 1rem;
}
#todo-input::placeholder {
color: rgba(255, 255, 255, 0.4);
}
/* Purple Add button */
#todo-form button {
padding: 0.6rem 1.2rem;
background: #6c63ff;
border: none;
border-radius: 6px;
color: #ffffff;
font-size: 1rem;
cursor: pointer;
transition: background 0.2s;
}
#todo-form button:hover {
background: #5a52d5;
}
/* Task list items with hover and delete button */
#todo-list {
list-style: none;
}
#todo-list li {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.7rem 1rem;
background: rgba(255, 255, 255, 0.05);
border-radius: 6px;
margin-bottom: 0.5rem;
transition: background 0.2s;
}
#todo-list li:hover {
background: rgba(255, 255, 255, 0.1);
}
#todo-list li .delete-btn {
background: none;
border: none;
color: #ff6b6b;
font-size: 1.2rem;
cursor: pointer;
padding: 0 0.3rem;
}
#todo-list li .delete-btn:hover {
color: #ff4757;
}
What does this code do?
The form uses display: flex to place the input and button side by side. flex: 1 on the input makes it stretch to fill all available space.
Each li uses flexbox with justify-content: space-between to push the task text left and the delete button right. The hover effect highlights which task you are about to interact with.
The delete button uses a red colour (#ff6b6b) that darkens on hover to signal a destructive action.
- Save style.css and check your browser.
- Your to-do form should have a styled input next to a purple Add button. Any existing tasks should appear as cards with a red x delete button on the right.
To-do items look unstyled?
If your tasks still appear as a plain bulleted list, check that you have list-style: none on #todo-list. Without it, the browser's default bullet points override your layout.
Also verify that your JavaScript adds the delete-btn class to the delete button. Open DevTools (Cmd+Option+I on macOS or Ctrl+Shift+I on Windows) and inspect a task to confirm.
Help me figure out why my to-do list styles aren't working.
Understand the responsive approach
You have already made your dashboard responsive without writing any extra code. The styles you just added use relative units throughout.
How is this responsive?
Your stylesheet uses three techniques that adapt to different screen sizes:
- clamp(2.5rem, 8vw, 4rem) scales the clock text between a minimum and maximum size based on viewport width.
- width: 90% with max-width: 600px keeps the dashboard readable on large screens and full-width on small ones.
- rem units for padding and margins scale proportionally with the user's base font size.
- Try resizing your browser window narrower and wider. The clock text and layout should adjust smoothly without breaking.
✔️ Awesome, I've got everything!
Great. Double check you have saved style.css and your dashboard looks polished in the browser.
ⓧ I'd like to double check the full code
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #0f0c29, #302b63, #24243e);
font-family: 'Segoe UI', sans-serif;
color: #ffffff;
}
.dashboard {
width: 90%;
max-width: 600px;
padding: 2rem;
}
.clock-section {
text-align: center;
margin-bottom: 2rem;
}
#time {
font-size: clamp(2.5rem, 8vw, 4rem);
font-weight: 700;
letter-spacing: 4px;
font-family: 'Segoe UI', monospace;
}
#date {
font-size: 1.1rem;
color: rgba(255, 255, 255, 0.6);
margin-top: 0.5rem;
}
.quote-section {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 2rem;
text-align: center;
}
#quote-text {
font-style: italic;
font-size: 1.1rem;
line-height: 1.6;
margin-bottom: 0.75rem;
}
#quote-author {
font-size: 0.9rem;
color: rgba(255, 255, 255, 0.6);
margin-bottom: 1rem;
}
#new-quote-btn {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
color: #ffffff;
padding: 0.5rem 1.2rem;
border-radius: 6px;
cursor: pointer;
font-size: 0.85rem;
transition: background 0.2s;
}
#new-quote-btn:hover {
background: rgba(255, 255, 255, 0.2);
}
.todo-section h2 {
margin-bottom: 1rem;
font-size: 1.3rem;
}
#todo-form {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
}
#todo-input {
flex: 1;
padding: 0.6rem 1rem;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 6px;
background: rgba(255, 255, 255, 0.05);
color: #ffffff;
font-size: 1rem;
}
#todo-input::placeholder {
color: rgba(255, 255, 255, 0.4);
}
#todo-form button {
padding: 0.6rem 1.2rem;
background: #6c63ff;
border: none;
border-radius: 6px;
color: #ffffff;
font-size: 1rem;
cursor: pointer;
transition: background 0.2s;
}
#todo-form button:hover {
background: #5a52d5;
}
#todo-list {
list-style: none;
}
#todo-list li {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.7rem 1rem;
background: rgba(255, 255, 255, 0.05);
border-radius: 6px;
margin-bottom: 0.5rem;
transition: background 0.2s;
}
#todo-list li:hover {
background: rgba(255, 255, 255, 0.1);
}
#todo-list li .delete-btn {
background: none;
border: none;
color: #ff6b6b;
font-size: 1.2rem;
cursor: pointer;
padding: 0 0.3rem;
}
#todo-list li .delete-btn:hover {
color: #ff4757;
}
Secret mission
Add a Smart Greeting and Focus Task
Your dashboard tells you the time and shows quotes, but it doesn't feel personal yet. In this secret mission, you'll add a greeting that changes based on the time of day and a focus task feature that keeps your most important goal front and center.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally on your computer, so there are no ongoing costs. Decide whether to keep your files as they are, pause your work, or delete everything.
Resources you used:
- personal-dashboard folder on your Desktop (containing index.html, style.css, and script.js).
- localStorage data in your browser (to-do list items and focus task).
- Live Server extension in VS Code.
Keep everything running
No action needed. Choose this if you want to keep using your dashboard or plan to continue building on it.
- Your personal-dashboard folder stays on your Desktop, ready to open anytime.
- Your to-do items and focus task remain saved in your browser's localStorage.
- You can set this dashboard as your browser's new tab page for daily use.
Pause - I'll come back to this later
Stop the Live Server to free up the port, but keep all your files intact for next time.
- In VS Code, click Port: 5500 in the bottom status bar to stop Live Server.
- Close the browser tab showing your dashboard.
- Your files and localStorage data will still be there when you come back.
Delete - I don't want to use this again
Remove all project files, clear saved data, and optionally uninstall the Live Server extension.
- Stop Live Server by clicking Port: 5500 in the VS Code status bar.
- Delete the project folder by running this command in your terminal:
rm -rf ~/Desktop/personal-dashboard
On Windows, run this instead:
Remove-Item -Recurse -Force ~\Desktop\personal-dashboard
- Clear your localStorage data by opening your browser's developer console (Cmd+Option+J on macOS or Ctrl+Shift+J on Windows) and running:
localStorage.removeItem('todos');
localStorage.removeItem('focus');
- To uninstall Live Server, open the Extensions panel in VS Code (Cmd+Shift+X on macOS or Ctrl+Shift+X on Windows), find Live Server in your installed extensions, and click Uninstall. Skip this if you plan to use Live Server for other web projects.
Nice Work!
Nice Work!
Nice work! You just built a fully functional personal dashboard using vanilla JavaScript. Every time you open this page, you see a live clock, a fresh quote, and your saved to-do list waiting for you.
You've learned how to:
- Built a live digital clock that updates every second using setInterval and the Date object.
- Fetched data from a real API using fetch and displayed it dynamically in the DOM.
- Created a persistent to-do list with add and delete functionality, saved across browser sessions using localStorage with JSON.stringify and JSON.parse.
- Secret Mission: Added conditional logic for time-based greetings and a focus task feature with localStorage persistence.
Ready to quiz yourself?