A practical companion guide for learning how browser localStorage works while building a persistent notes app with plain JavaScript.
Prefer following along visually? Start with the complete video, then use the written guide as a reference whenever you need it.
Refreshing a page normally resets the values created by JavaScript. In this guide, you will learn how to keep small pieces of browser data available after a refresh by building a complete notes app with plain HTML, CSS, and JavaScript.
The project begins with a prepared notes interface. The JavaScript is added gradually so each step has a visible result: first saving one value, then restoring it, then managing a collection of notes. By the end, the app can create, edit, delete, save, restore, and safely recover notes.
In Chapter 1 of the video, the HTML and CSS are already prepared so we can focus on JavaScript. You can copy the complete starting point below into your own project. Create these two files, index.html and style.css, in the same folder, then create an empty index.js file. The HTML already connects the JavaScript file at the bottom.
index.html<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Focus Notes</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="app-shell">
<header class="hero">
<div>
<p class="eyebrow">Your quiet space to think</p>
<h1 class="heading">Focus Notes</h1>
<p class="info-text">
Capture an idea, keep it automatically, and double-click any note
when you are done with it.
</p>
</div>
<div class="status-pill">Saved locally</div>
</header>
<section class="app" id="app" aria-label="Your notes">
<button class="btn" id="btn" type="button" aria-label="Add a note">
+ New note
</button>
</section>
</main>
<script src="index.js"></script>
</body>
</html>
style.cssThe HTML and CSS are only the starting point. The New Note button will not work yet because index.js is still empty. That is intentional: from this point in the video, the focus is on adding the JavaScript behavior and making the data survive a refresh.
localStorage stores data for a websitesetItem(), getItem(), and removeItem()JSON.stringify() and JSON.parse() store arrays and objectstry...catch and Array.isArray() make loading saferlocalStorage is useful and when it is the wrong toolStart with a normal JavaScript variable:
let note = "Plan tomorrow's tasks";
console.log(note);
The value exists in the current page memory, so the Console can display it. If you change the variable manually in DevTools, the change lasts only until the page is refreshed. A refresh runs the script again and creates the original variable again.
That is the problem localStorage solves. It gives a website a small, persistent key-value store in the browser.
Use setItem() to save a value under a key:
let note = "Plan tomorrow's tasks";
localStorage.setItem("note", note);
The first argument, "note", is the key. The second argument is the value. Later, use the same key with getItem():
const savedNote = localStorage.getItem("note");
console.log(savedNote);
The browser keeps this value for the current website origin, so it can still be read after a refresh. In DevTools, open the Application panel, expand Local Storage, and select the current website to see the key and value.
The page does not need to change visually for this to work. Storage happens behind the scenes; the next step is connecting that stored data to the notes interface.
Once the app can contain more than one note, store each note as an object. A simple note needs a stable ID and its text:
{
id: "note-1",
text: "Plan tomorrow's tasks"
}
Keep the collection in an array while the page is running:
const notes = [
{ id: "note-1", text: "Plan tomorrow's tasks" },
{ id: "note-2", text: "Buy groceries" },
];
The ID matters because it lets the app find the correct object when a note is edited or deleted. The interface can then render every object into a textarea inside the prepared notes container.
localStorage does not store JavaScript arrays or objects directly. Convert the notes array into a JSON string before saving it:
function saveNotes(notes) {
localStorage.setItem("notes", JSON.stringify(notes));
}
When the page loads, read the string and convert it back into JavaScript data:
function getNotes() {
const storedNotes = localStorage.getItem("notes");
return storedNotes ? JSON.parse(storedNotes) : [];
}
The complete flow is:
JavaScript array → JSON.stringify() → localStorage string
localStorage string → JSON.parse() → JavaScript array
Call saveNotes() after every change. Call getNotes() when the app starts, then render the returned array so saved notes appear after a refresh.
To delete a note, find its position in the array, remove the object, remove its element from the page, and save the shorter array:
function deleteNote(id, element) {
const index = notes.findIndex((note) => note.id === id);
notes.splice(index, 1);
element.remove();
saveNotes(notes);
}
The tutorial connects this function to a double-click on each note. The same ID is used by the data object and its textarea, so the correct note is removed from both places.
When the final note is deleted, remove the storage key instead of leaving an unnecessary empty array:
function saveNotes(notes) {
if (notes.length === 0) {
localStorage.removeItem("notes");
return;
}
localStorage.setItem("notes", JSON.stringify(notes));
}
Stored data can be missing, invalid, or valid JSON with the wrong shape. A direct call to JSON.parse() can throw an error and stop the rest of the app from running.
Protect the loading function with a missing-data guard, try...catch, and an array check:
function getNotes() {
const storedNotes = localStorage.getItem("notes");
if (!storedNotes) return [];
try {
const parsedNotes = JSON.parse(storedNotes);
return Array.isArray(parsedNotes) ? parsedNotes : [];
} catch (error) {
localStorage.removeItem("notes");
return [];
}
}
If the value is broken, the app clears the damaged entry and starts with an empty collection instead of failing completely. Array.isArray() is important because valid JSON can still be an object, string, or number rather than the array the app expects.
localStorage is useful for small, convenient, non-sensitive browser data such as:
It is not a secure place for passwords, authentication tokens, private messages, or other sensitive information. Data stored there belongs to the browser profile and can be inspected or changed by the user.
Your notes app is complete when it can:
The main pattern to remember is simple: keep your data in JavaScript, serialize it when saving, parse it when loading, and update the interface whenever the data changes.
:root {
color-scheme: dark;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
"Segoe UI", sans-serif;
background: #090b16;
color: #f7f7ff;
}
* {
box-sizing: border-box;
}
body {
min-height: 100vh;
margin: 0;
padding: 72px 24px;
background:
radial-gradient(circle at 12% 10%, rgba(124, 92, 255, 0.3), transparent 34%),
radial-gradient(circle at 88% 18%, rgba(0, 209, 255, 0.2), transparent 30%),
linear-gradient(145deg, #090b16 0%, #11142a 50%, #080a14 100%);
}
body::before {
position: fixed;
inset: 0;
z-index: -1;
background-image: linear-gradient(rgba(255, 255, 255, 0.025) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.025) 1px, transparent 1px);
background-size: 42px 42px;
content: "";
mask-image: linear-gradient(to bottom, black, transparent 82%);
}
.app-shell {
width: min(1120px, 100%);
margin: 0 auto;
padding: 38px;
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 32px;
background: rgba(15, 18, 37, 0.72);
box-shadow: 0 30px 90px rgba(0, 0, 0, 0.38);
backdrop-filter: blur(24px);
}
.hero {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 32px;
padding: 6px 4px 34px;
}
.eyebrow {
margin: 0 0 12px;
color: #a99cff;
font-size: 0.78rem;
font-weight: 800;
letter-spacing: 0.14em;
text-transform: uppercase;
}
.heading {
margin: 0;
font-size: clamp(2.5rem, 6vw, 4.7rem);
letter-spacing: -0.055em;
line-height: 0.96;
}
.info-text {
max-width: 610px;
margin: 18px 0 0;
color: #aeb2ca;
font-size: 1rem;
line-height: 1.7;
}
.status-pill {
display: inline-flex;
flex: 0 0 auto;
align-items: center;
gap: 9px;
padding: 10px 14px;
border: 1px solid rgba(122, 255, 196, 0.18);
border-radius: 999px;
background: rgba(52, 211, 153, 0.08);
color: #9cf5cc;
font-size: 0.78rem;
font-weight: 750;
}
.status-pill::before {
content: "";
width: 8px;
height: 8px;
border-radius: 50%;
background: #4ade80;
box-shadow: 0 0 14px #4ade80;
}
.app {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 18px;
}
.note,
.btn {
min-height: 230px;
border-radius: 22px;
}
.note {
width: 100%;
padding: 24px;
resize: none;
border: 1px solid rgba(255, 255, 255, 0.1);
outline: none;
background: linear-gradient(145deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.045));
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
color: #f5f3ff;
font: inherit;
font-size: 1rem;
line-height: 1.65;
transition: border-color 180ms ease, transform 180ms ease, box-shadow 180ms ease;
}
.note::placeholder {
color: #737994;
}
.note:hover,
.note:focus {
border-color: rgba(154, 134, 255, 0.55);
box-shadow: 0 18px 40px rgba(4, 6, 18, 0.34), 0 0 0 4px rgba(124, 92, 255, 0.08);
transform: translateY(-3px);
}
.btn {
display: grid;
place-content: center;
gap: 12px;
border: 1px dashed rgba(169, 156, 255, 0.48);
background: rgba(124, 92, 255, 0.07);
color: #b9adff;
cursor: pointer;
font-size: 0.82rem;
font-weight: 800;
letter-spacing: 0.1em;
text-transform: uppercase;
transition: border-color 180ms ease, background 180ms ease, transform 180ms ease;
}
.btn:hover {
border-color: #a99cff;
background: rgba(124, 92, 255, 0.16);
transform: translateY(-3px);
}
@media (max-width: 680px) {
body {
padding: 28px 14px;
}
.app-shell {
padding: 26px 20px;
border-radius: 24px;
}
.hero {
flex-direction: column;
}
}