Skip to content

The Tour Favorites List with localStorage

The favorites list lets visitors save tours they are interested in. The saved list survives page reloads — close the tab, come back, and the favorites are still there. This combines everything from Module 07: events, delegation, DOM manipulation, and localStorage.

  • Each tour card has an “Add to Favorites” button
  • Clicking it adds the tour to a favorites list shown on the page
  • Each favorite has a “Remove” button
  • The list persists across page reloads via localStorage

Every tour card needs a button with class="btn-add-favorite". Add it to the JS-rendered cards by updating renderTourCard in main.js:

function renderTourCard(tour) {
const status = tour.available ? 'Available' : 'Sold Out';
return `
<article class="tour-card" data-category="${tour.category}">
<div class="tour-card-image-wrap">
<img class="tour-card-image" src="${tour.img}" alt="${tour.alt}">
</div>
<div class="tour-card-body">
<h3>${tour.name}</h3>
<p class="tour-price">${formatPrice(tour.price)}</p>
<p class="tour-status">${status}</p>
<button class="btn-add-favorite">Add to Favorites</button>
</div>
</article>
`;
}

Also add the button to every hardcoded tour card in index.html and tours.html — place it inside the card’s CTA or footer div, after the existing link:

<!-- index.html — inside .tour-card-cta -->
<div class="tour-card-cta">
<a href="tours.html" class="btn btn-primary">View Tour</a>
<button class="btn-add-favorite">Add to Favorites</button>
</div>
<!-- tours.html — inside .tour-card-footer -->
<div class="tour-card-footer">
<div class="tour-price">$65 <span class="tour-price-label">per person</span></div>
<a href="contact.html" class="btn btn-primary">Book this tour</a>
<button class="btn-add-favorite">Add to Favorites</button>
</div>

Add a favorites section to index.html and tours.html — place it after the main tours section, before the footer:

<section class="favorites-section section-padding">
<div class="content-wrapper">
<h2>Your Saved Tours</h2>
<ul class="favorites-list"></ul>
</div>
</section>

The <ul> starts empty — renderFavorites() populates it immediately on page load.

Add the following to styles.css:

/* === Favorites === */
.btn-add-favorite {
margin-top: var(--space-sm);
padding: var(--space-xs) var(--space-md);
background: none;
border: 2px solid var(--color-primary);
color: var(--color-primary);
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.875rem;
font-weight: 600;
transition: background-color 0.2s, color 0.2s;
}
.btn-add-favorite:hover {
background-color: var(--color-primary);
color: var(--color-white);
}
.favorites-list {
list-style: none;
padding: 0;
margin: 0;
}
.favorites-list li {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--space-sm) 0;
border-bottom: 1px solid var(--color-border);
font-weight: 600;
}
.favorites-empty {
color: var(--color-text-muted);
font-style: italic;
font-weight: 400;
}
.btn-remove-favorite {
padding: var(--space-xs) var(--space-sm);
background: none;
border: 1px solid var(--color-error);
color: var(--color-error);
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.8rem;
font-weight: 600;
transition: background-color 0.2s, color 0.2s;
}
.btn-remove-favorite:hover {
background-color: var(--color-error);
color: var(--color-white);
}

The simplest structure: an array of tour name strings.

// In localStorage, stored as:
// '["Cascade Ridge Hike","Summit Loop Trek"]'
// In JavaScript:
let favorites = []; // will be populated from localStorage on load

When a tour is added, push its name. When removed, filter it out. Persist after every change.

Read from localStorage when the page loads:

const FAVORITES_KEY = 'sto-favorites';
function loadFavorites() {
const saved = localStorage.getItem(FAVORITES_KEY);
return saved ? JSON.parse(saved) : [];
}
let favorites = loadFavorites();

Save after every mutation, then re-render the list:

function saveFavorites() {
localStorage.setItem(FAVORITES_KEY, JSON.stringify(favorites));
}
const favoritesList = document.querySelector('.favorites-list');
function renderFavorites() {
if (!favoritesList) return;
favoritesList.innerHTML = '';
if (favorites.length === 0) {
favoritesList.innerHTML = '<li class="favorites-empty">No favorites saved yet.</li>';
return;
}
favorites.forEach(name => {
const li = document.createElement('li');
li.textContent = name;
const removeBtn = document.createElement('button');
removeBtn.textContent = 'Remove';
removeBtn.dataset.tour = name;
removeBtn.className = 'btn-remove-favorite';
li.appendChild(removeBtn);
favoritesList.appendChild(li);
});
}

favoritesList.innerHTML = '' clears the current list before re-rendering — simple and reliable.

container is already defined earlier in main.js — it is the .tours-grid element used to render tour cards. Register the add-favorite listener on the same element using closest('.btn-add-favorite') as the guard:

if (container) {
container.addEventListener('click', (event) => {
const btn = event.target.closest('.btn-add-favorite');
if (!btn) return;
const card = btn.closest('.tour-card');
if (!card) return;
const name = card.querySelector('h3').textContent;
if (!favorites.includes(name)) {
favorites.push(name);
saveFavorites();
renderFavorites();
}
});
}

includes prevents duplicates — if the tour is already in the array, do nothing.

One click listener on the favorites list:

if (favoritesList) {
favoritesList.addEventListener('click', (event) => {
const btn = event.target.closest('.btn-remove-favorite');
if (!btn) return;
favorites = favorites.filter(f => f !== btn.dataset.tour);
saveFavorites();
renderFavorites();
});
}

filter returns a new array with the matching item removed. Reassigning favorites replaces the old array.

container is already declared in main.js from the tour card rendering section — do not redeclare it. The favorites code adds to the existing if (container) block and introduces its own new variables and functions.

const FAVORITES_KEY = 'sto-favorites';
const favoritesList = document.querySelector('.favorites-list');
function loadFavorites() {
const saved = localStorage.getItem(FAVORITES_KEY);
return saved ? JSON.parse(saved) : [];
}
function saveFavorites() {
localStorage.setItem(FAVORITES_KEY, JSON.stringify(favorites));
}
function renderFavorites() {
if (!favoritesList) return;
favoritesList.innerHTML = '';
if (favorites.length === 0) {
favoritesList.innerHTML = '<li class="favorites-empty">No favorites saved yet.</li>';
return;
}
favorites.forEach(name => {
const li = document.createElement('li');
li.textContent = name;
const removeBtn = document.createElement('button');
removeBtn.textContent = 'Remove';
removeBtn.dataset.tour = name;
removeBtn.className = 'btn-remove-favorite';
li.appendChild(removeBtn);
favoritesList.appendChild(li);
});
}
let favorites = loadFavorites();
renderFavorites();
if (container) {
container.addEventListener('click', (event) => {
const btn = event.target.closest('.btn-add-favorite');
if (!btn) return;
const card = btn.closest('.tour-card');
if (!card) return;
const name = card.querySelector('h3').textContent;
if (!favorites.includes(name)) {
favorites.push(name);
saveFavorites();
renderFavorites();
}
});
}
if (favoritesList) {
favoritesList.addEventListener('click', (event) => {
const btn = event.target.closest('.btn-remove-favorite');
if (!btn) return;
favorites = favorites.filter(f => f !== btn.dataset.tour);
saveFavorites();
renderFavorites();
});
}
  1. Add a favorite — it appears in the list
  2. Reload the page — it is still there
  3. Open DevTools → Application (Chrome) or Storage (Firefox) → Local Storage → your site origin — the key and JSON value are visible
  4. Remove a favorite — it disappears from both the list and localStorage
  5. Add all three tours — then reload — all three persist
  6. Navigate to the tours page — the same favorites appear (requires local server)
  • The data model is an array of strings — simple enough to serialize with JSON.stringify, simple enough to search with includes.
  • loadFavorites() reads from localStorage with a fallback [] — always returns an array.
  • renderFavorites() rebuilds the DOM from the array on every change — one source of truth.
  • saveFavorites() is called after every mutation — add or remove always triggers a persist.
  • container is already in scope from the tour rendering section — the add-favorite listener registers on the same element, not a new selection.
  • Two delegated listeners: one on the tours container for adding, one on the favorites list for removing.
  • localStorage is scoped to the origin — on a real server all pages share the same store. With file:// URLs, browsers treat each file as a separate origin, so cross-page persistence only works when served from localhost or a deployed host.