#2 HTML+ CSS+JSS using Code Pen
π HTML, CSS & JavaScript — 10‑Module CodePen Course
A hands-on, progressive course you can complete entirely in CodePen. Each module includes: goals, concepts, a minimal HTML/CSS/JS starter you can paste into the three CodePen panels, and practice tasks. Difficulty increases step by step.
π§° Before You Start: CodePen Tips
-
Open a new Pen. Use the three panels: HTML, CSS, JS.
-
CodePen auto-injects
<html>
,<head>
, and<body>
for you. You can paste full HTML documents, but simpler is to paste only what goes inside<body>
. -
Open the Console (bottom right ▸ ▸ Console) to see
console.log()
output. -
If you need fonts/icons later: Settings → HTML → Add external resources (e.g., Google Fonts, Font Awesome).
Module 1 — HTML Foundations
Goal: Understand core tags, semantic structure, links, images, lists, and basic accessibility.
Key concepts: Elements, attributes, headings, paragraphs, lists, links (<a>
), images (<img>
), semantic tags (<header> <nav> <main> <section> <article> <footer>
), alt text.
HTML (paste in HTML panel):
<header>
<h1>My First Site</h1>
<nav>
<a href="#home">Home</a> · <a href="#about">About</a> · <a href="#contact">Contact</a>
</nav>
</header>
<main>
<section id="home">
<h2>Welcome π</h2>
<p>This page shows basic <strong>HTML</strong> building blocks.</p>
<img src="https://images.unsplash.com/photo-1526378722484-bd91ca387e72?w=800" alt="Laptop on desk" width="320">
</section>
<section id="about">
<h2>About</h2>
<p>HTML provides <em>structure</em> to content. Here is a list:</p>
<ul>
<li>Headings and paragraphs</li>
<li>Lists</li>
<li>Links and images</li>
</ul>
</section>
<section id="contact">
<h2>Contact</h2>
<p>Email: <a href="mailto:hello@example.com">hello@example.com</a></p>
</section>
</main>
<footer>
<small>© 2025 My First Site</small>
</footer>
CSS: (optional for now; add in Module 2)
JS: (none yet)
Practice: Add a new section with an ordered list of your learning goals. Add proper alt
text to any new images.
Module 2 — CSS Basics & The Box Model
Goal: Style text and layout basics; understand padding, border, margin, display
.
Key concepts: Selectors, properties, cascade & specificity, margin/padding/border
, display:block/inline/inline-block
.
HTML:
<header class="site-header">
<h1>Stylish Site</h1>
<p class="tagline">Learning CSS one step at a time.</p>
</header>
<main class="content">
<article class="card">
<h2>Card Title</h2>
<p>Cards are boxes with padding, border, and margin.</p>
<a class="btn" href="#">Read more</a>
</article>
<article class="card">
<h2>Another Card</h2>
<p>Notice consistent spacing and typography.</p>
<a class="btn" href="#">Details</a>
</article>
</main>
CSS (paste in CSS panel):
:root { --bg: #f7fafc; --ink: #1a202c; --brand: #2563eb; }
* { box-sizing: border-box; }
body { font-family: system-ui, Arial, sans-serif; background: var(--bg); color: var(--ink); line-height: 1.6; margin: 0; }
.site-header { text-align: center; padding: 2rem 1rem; }
.tagline { color: #4a5568; margin-top: .25rem; }
.content { display: flex; gap: 1rem; padding: 1rem; max-width: 960px; margin: 0 auto; flex-wrap: wrap; }
.card { background: white; border: 1px solid #e2e8f0; border-radius: 12px; padding: 1rem; width: 300px; }
.card h2 { margin-top: 0; }
.btn { display: inline-block; background: var(--brand); color: white; padding: .5rem .75rem; border-radius: 8px; text-decoration: none; }
.btn:hover { filter: brightness(.95); }
JS: (none yet)
Practice: Add a third card. Change the --brand
color. Add :hover
style to .card
.
Module 3 — JavaScript Fundamentals
Goal: Write variables, functions, and handle click events.
Key concepts: const/let
, functions, events, DOM selection, console.log
.
HTML:
<h1>JS Basics Playground</h1>
<button id="helloBtn">Say Hello</button>
<p id="output"></p>
CSS:
body { font-family: system-ui; padding: 1.5rem; }
#output { margin-top: 1rem; font-weight: 600; }
button { padding: .6rem 1rem; border: 0; border-radius: 8px; cursor: pointer; }
JS (paste in JS panel):
const btn = document.getElementById('helloBtn');
const out = document.getElementById('output');
function greet(name = 'friend') { return `Hello, ${name}!`; }
btn.addEventListener('click', () => {
const name = prompt('Your name?');
out.textContent = greet(name || undefined);
console.log('Greeted:', name);
});
Practice: Add another button that clears the message. Log the current time each click.
Module 4 — DOM Manipulation & Event Delegation
Goal: Create, insert, and remove elements dynamically.
Key concepts: document.createElement
, append
, remove
, event delegation (.addEventListener
on a parent), dataset attributes.
HTML:
<h1>Mini To‑Do</h1>
<form id="todoForm">
<input id="todoInput" placeholder="Add a task" required>
<button>Add</button>
</form>
<ul id="list"></ul>
CSS:
body { font-family: system-ui; max-width: 600px; margin: 2rem auto; padding: 0 1rem; }
li { display: flex; justify-content: space-between; padding: .5rem .75rem; border: 1px solid #e2e8f0; border-radius: 8px; margin-top: .5rem; }
button { cursor: pointer; }
JS:
const form = document.getElementById('todoForm');
const input = document.getElementById('todoInput');
const list = document.getElementById('list');
form.addEventListener('submit', e => {
e.preventDefault();
const text = input.value.trim();
if (!text) return;
const li = document.createElement('li');
li.innerHTML = `<span>${text}</span> <button data-action="remove">✖</button>`;
list.append(li);
input.value = '';
});
list.addEventListener('click', e => {
if (e.target.matches('button[data-action="remove"]')) {
e.target.closest('li').remove();
}
});
Practice: Add a “complete” toggle that strikes through text. Persist tasks to localStorage
(hint in Module 9).
Module 5 — Responsive Layouts: Flexbox & Grid
Goal: Build responsive layouts that adapt to screen size.
Key concepts: Flexbox (axis, gap, wrap, alignment), CSS Grid (columns, auto-fit, minmax), media queries.
HTML:
<header class="topbar"><h1>Photo Grid</h1></header>
<main class="gallery">
<img src="https://picsum.photos/seed/1/600/400" alt="Random 1">
<img src="https://picsum.photos/seed/2/600/400" alt="Random 2">
<img src="https://picsum.photos/seed/3/600/400" alt="Random 3">
<img src="https://picsum.photos/seed/4/600/400" alt="Random 4">
<img src="https://picsum.photos/seed/5/600/400" alt="Random 5">
<img src="https://picsum.photos/seed/6/600/400" alt="Random 6">
</main>
<footer class="foot">© 2025</footer>
CSS:
body { margin: 0; font-family: system-ui; }
.topbar, .foot { padding: 1rem; text-align: center; background: #111; color: #fff; }
.gallery { display: grid; gap: 10px; padding: 10px; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }
.gallery img { width: 100%; height: 200px; object-fit: cover; border-radius: 12px; }
JS: (none required)
Practice: Add a responsive card row using Flexbox below the grid. At <600px, make images taller with a media query.
Module 6 — Forms & Client‑Side Validation
Goal: Create accessible forms and validate inputs.
Key concepts: Labels, required
, HTML5 input types, constraint API, custom messages.
HTML:
<h1>Sign Up</h1>
<form id="signup" novalidate>
<label>Email
<input type="email" id="email" required placeholder="you@example.com">
</label>
<label>Password
<input type="password" id="pass" required minlength="6" placeholder="At least 6 chars">
</label>
<label>Confirm Password
<input type="password" id="confirm" required>
</label>
<button>Create Account</button>
<p id="errors" role="alert" aria-live="polite"></p>
</form>
CSS:
form { display: grid; gap: .75rem; max-width: 360px; margin: 2rem auto; font-family: system-ui; }
input { width: 100%; padding: .6rem; border: 1px solid #cbd5e1; border-radius: 8px; }
#errors { color: #b91c1c; font-weight: 600; }
button { padding: .6rem 1rem; border: 0; border-radius: 8px; cursor: pointer; }
JS:
const form = document.getElementById('signup');
const email = document.getElementById('email');
const pass = document.getElementById('pass');
const confirmPass = document.getElementById('confirm');
const errors = document.getElementById('errors');
form.addEventListener('submit', e => {
errors.textContent = '';
if (!email.checkValidity()) {
e.preventDefault();
errors.textContent = 'Please enter a valid email.';
return;
}
if (pass.value.length < 6) {
e.preventDefault();
errors.textContent = 'Password must be at least 6 characters.';
return;
}
if (pass.value !== confirmPass.value) {
e.preventDefault();
errors.textContent = 'Passwords do not match.';
}
});
Practice: Add real-time validation (on input
) that clears errors as the user types.
Module 7 — Reusable Components, BEM & Theming
Goal: Create reusable UI (cards, buttons, tabs), organize CSS with BEM, and add light/dark theme via CSS variables.
Key concepts: BEM naming (block__element--modifier
), CSS variables, toggling a theme class, tabs component.
HTML:
<header class="site">
<h1>Components & Themes</h1>
<button id="themeToggle" class="btn btn--ghost">Toggle Theme</button>
</header>
<section class="tabs" data-tabs>
<nav class="tabs__list">
<button class="tabs__tab tabs__tab--active" data-tab="one">One</button>
<button class="tabs__tab" data-tab="two">Two</button>
<button class="tabs__tab" data-tab="three">Three</button>
</nav>
<div class="tabs__panel tabs__panel--active" data-panel="one">Panel One Content</div>
<div class="tabs__panel" data-panel="two">Panel Two Content</div>
<div class="tabs__panel" data-panel="three">Panel Three Content</div>
</section>
CSS:
:root { --bg:#ffffff; --ink:#111827; --card:#ffffff; --border:#e5e7eb; }
.dark { --bg:#0b1220; --ink:#e5e7eb; --card:#111827; --border:#374151; }
body { background: var(--bg); color: var(--ink); font-family: system-ui; margin: 0; }
.site { display: flex; justify-content: space-between; align-items: center; padding: 1rem; border-bottom: 1px solid var(--border); }
.btn { padding:.5rem .75rem; border-radius: 10px; border:1px solid var(--border); background: var(--card); cursor: pointer; }
.btn--ghost { background: transparent; }
.tabs { max-width: 720px; margin: 1rem auto; padding: 1rem; }
.tabs__list { display: flex; gap:.5rem; margin-bottom: .75rem; }
.tabs__tab { border:1px solid var(--border); background: var(--card); padding:.5rem .75rem; border-radius: 8px; cursor: pointer; }
.tabs__tab--active { outline: 2px solid #60a5fa; }
.tabs__panel { display: none; border:1px solid var(--border); padding: 1rem; border-radius: 12px; }
.tabs__panel--active { display: block; }
JS:
const root = document.documentElement;
const themeBtn = document.getElementById('themeToggle');
themeBtn.addEventListener('click', () => {
document.body.classList.toggle('dark');
});
const tabs = document.querySelector('[data-tabs]');
const tabButtons = tabs.querySelectorAll('.tabs__tab');
const panels = tabs.querySelectorAll('.tabs__panel');
tabs.addEventListener('click', (e) => {
const btn = e.target.closest('.tabs__tab');
if (!btn) return;
const target = btn.dataset.tab;
tabButtons.forEach(b => b.classList.toggle('tabs__tab--active', b === btn));
panels.forEach(p => p.classList.toggle('tabs__panel--active', p.dataset.panel === target));
});
Practice: Add a .btn--primary
modifier and use CSS variables to change its background in dark mode.
Module 8 — Fetch API, JSON & Loading States
Goal: Request data from an API and render it.
Key concepts: fetch
, async/await
, error handling, skeleton/loading UI.
HTML:
<h1>Posts (from JSONPlaceholder)</h1>
<div id="status">Loading…</div>
<section id="posts" class="posts"></section>
CSS:
body { font-family: system-ui; max-width: 900px; margin: 2rem auto; padding: 0 1rem; }
.posts { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1rem; }
.post { border:1px solid #e5e7eb; border-radius: 12px; padding: 1rem; background: #fff; }
JS:
const status = document.getElementById('status');
const postsEl = document.getElementById('posts');
(async function load() {
try {
const res = await fetch('https://jsonplaceholder.typicode.com/posts?_limit=8');
if (!res.ok) throw new Error('Network error');
const posts = await res.json();
status.textContent = '';
postsEl.innerHTML = posts.map(p => `
<article class="post">
<h3>${p.title}</h3>
<p>${p.body}</p>
</article>
`).join('');
} catch (err) {
status.textContent = 'Failed to load posts.';
console.error(err);
}
})();
Practice: Add a search input that filters posts by title (client-side). Show a retry button on error.
Module 9 — State, localStorage & Simple Hash Routing
Goal: Persist data locally and build a tiny SPA with #routes
.
Key concepts: localStorage
, serialization, reactive rendering, window.onhashchange
.
HTML:
<header class="spa">
<a href="#/home">Home</a> · <a href="#/favorites">Favorites</a> · <a href="#/about">About</a>
</header>
<main id="view"></main>
CSS:
body { font-family: system-ui; }
.spa { display:flex; gap:1rem; padding:1rem; border-bottom:1px solid #e5e7eb; }
.card { border:1px solid #e5e7eb; border-radius: 10px; padding: .75rem; margin:.5rem 0; }
.btn { padding:.4rem .7rem; border:0; border-radius:8px; cursor:pointer; }
JS:
const view = document.getElementById('view');
const KEY = 'favorites:v1';
let favorites = JSON.parse(localStorage.getItem(KEY) || '[]');
function save() { localStorage.setItem(KEY, JSON.stringify(favorites)); }
function toggleFav(id) {
favorites = favorites.includes(id) ? favorites.filter(x => x !== id) : [...favorites, id];
save();
render();
}
const pages = {
'/home': () => {
const items = Array.from({length: 5}, (_,i) => ({ id: i+1, title: `Item ${i+1}` }));
return `
<h1>Home</h1>
${items.map(it => `
<div class="card">
<strong>${it.title}</strong>
<button class="btn" data-id="${it.id}">
${favorites.includes(it.id) ? '★ Remove' : '☆ Fav'}
</button>
</div>`).join('')}
`;
},
'/favorites': () => `
<h1>Favorites</h1>
<pre>${JSON.stringify(favorites, null, 2)}</pre>
`,
'/about': () => `<h1>About</h1><p>Simple hash router demo.</p>`
};
function render() {
const route = location.hash.replace('#', '') || '/home';
const page = pages[route] || (() => '<h1>404</h1>');
view.innerHTML = page();
}
window.addEventListener('hashchange', render);
view.addEventListener('click', (e) => {
const btn = e.target.closest('button[data-id]');
if (btn) toggleFav(Number(btn.dataset.id));
});
render();
Practice: Add a /settings
route to clear favorites. Persist a theme choice (reuse Module 7 pattern).
π Module 10 — Notes App Code
π HTML (structure)
<div class="app">
<h1>π My Notes</h1>
<!-- Input Section -->
<div class="note-input">
<textarea id="noteText" placeholder="Write a note..."></textarea>
<button id="addNoteBtn">Add Note</button>
</div>
<!-- Search -->
<input type="text" id="searchBox" placeholder="Search notes...">
<!-- Notes List -->
<div id="notesContainer" class="notes"></div>
</div>
<div class="app">
<h1>π My Notes</h1>
<!-- Input Section -->
<div class="note-input">
<textarea id="noteText" placeholder="Write a note..."></textarea>
<button id="addNoteBtn">Add Note</button>
</div>
<!-- Search -->
<input type="text" id="searchBox" placeholder="Search notes...">
<!-- Notes List -->
<div id="notesContainer" class="notes"></div>
</div>
π¨ CSS (style & layout)
body {
font-family: Arial, sans-serif;
background: #f4f7fb;
margin: 0;
padding: 20px;
display: flex;
justify-content: center;
}
.app {
max-width: 700px;
width: 100%;
background: #fff;
padding: 20px;
border-radius: 12px;
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
}
h1 {
text-align: center;
}
.note-input {
display: flex;
gap: 10px;
margin-bottom: 15px;
}
.note-input textarea {
flex: 1;
padding: 10px;
border-radius: 8px;
border: 1px solid #ddd;
resize: none;
height: 60px;
}
.note-input button {
background: #007bff;
border: none;
color: white;
padding: 12px 18px;
border-radius: 8px;
cursor: pointer;
}
.note-input button:hover {
background: #0056b3;
}
#searchBox {
width: 100%;
padding: 10px;
margin-bottom: 15px;
border-radius: 8px;
border: 1px solid #ddd;
}
.notes {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 15px;
}
.note {
background: #fdfdfd;
padding: 15px;
border-radius: 10px;
box-shadow: 0 2px 6px rgba(0,0,0,0.1);
position: relative;
}
.note p {
margin: 0;
white-space: pre-wrap;
}
.note .actions {
margin-top: 10px;
display: flex;
justify-content: space-between;
}
.note button {
border: none;
background: #eee;
padding: 5px 10px;
border-radius: 6px;
cursor: pointer;
font-size: 12px;
}
.note button:hover {
background: #ddd;
}
body {
font-family: Arial, sans-serif;
background: #f4f7fb;
margin: 0;
padding: 20px;
display: flex;
justify-content: center;
}
.app {
max-width: 700px;
width: 100%;
background: #fff;
padding: 20px;
border-radius: 12px;
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
}
h1 {
text-align: center;
}
.note-input {
display: flex;
gap: 10px;
margin-bottom: 15px;
}
.note-input textarea {
flex: 1;
padding: 10px;
border-radius: 8px;
border: 1px solid #ddd;
resize: none;
height: 60px;
}
.note-input button {
background: #007bff;
border: none;
color: white;
padding: 12px 18px;
border-radius: 8px;
cursor: pointer;
}
.note-input button:hover {
background: #0056b3;
}
#searchBox {
width: 100%;
padding: 10px;
margin-bottom: 15px;
border-radius: 8px;
border: 1px solid #ddd;
}
.notes {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 15px;
}
.note {
background: #fdfdfd;
padding: 15px;
border-radius: 10px;
box-shadow: 0 2px 6px rgba(0,0,0,0.1);
position: relative;
}
.note p {
margin: 0;
white-space: pre-wrap;
}
.note .actions {
margin-top: 10px;
display: flex;
justify-content: space-between;
}
.note button {
border: none;
background: #eee;
padding: 5px 10px;
border-radius: 6px;
cursor: pointer;
font-size: 12px;
}
.note button:hover {
background: #ddd;
}
⚡ JavaScript (logic)
const noteText = document.getElementById("noteText");
const addNoteBtn = document.getElementById("addNoteBtn");
const notesContainer = document.getElementById("notesContainer");
const searchBox = document.getElementById("searchBox");
let notes = JSON.parse(localStorage.getItem("notes")) || [];
// Save notes to localStorage
function saveNotes() {
localStorage.setItem("notes", JSON.stringify(notes));
}
// Render notes
function renderNotes(filter = "") {
notesContainer.innerHTML = "";
notes
.filter(note => note.text.toLowerCase().includes(filter.toLowerCase()))
.forEach((note, index) => {
const noteEl = document.createElement("div");
noteEl.classList.add("note");
const textEl = document.createElement("p");
textEl.textContent = note.text;
const actionsEl = document.createElement("div");
actionsEl.classList.add("actions");
const editBtn = document.createElement("button");
editBtn.textContent = "Edit";
editBtn.onclick = () => {
const newText = prompt("Edit note:", note.text);
if (newText !== null) {
notes[index].text = newText.trim();
saveNotes();
renderNotes(searchBox.value);
}
};
const deleteBtn = document.createElement("button");
deleteBtn.textContent = "Delete";
deleteBtn.onclick = () => {
notes.splice(index, 1);
saveNotes();
renderNotes(searchBox.value);
};
actionsEl.appendChild(editBtn);
actionsEl.appendChild(deleteBtn);
noteEl.appendChild(textEl);
noteEl.appendChild(actionsEl);
notesContainer.appendChild(noteEl);
});
}
// Add note
addNoteBtn.addEventListener("click", () => {
const text = noteText.value.trim();
if (text) {
notes.push({ text });
saveNotes();
renderNotes(searchBox.value);
noteText.value = "";
}
});
// Search notes
searchBox.addEventListener("input", (e) => {
renderNotes(e.target.value);
});
// Initial render
renderNotes();
const noteText = document.getElementById("noteText");
const addNoteBtn = document.getElementById("addNoteBtn");
const notesContainer = document.getElementById("notesContainer");
const searchBox = document.getElementById("searchBox");
let notes = JSON.parse(localStorage.getItem("notes")) || [];
// Save notes to localStorage
function saveNotes() {
localStorage.setItem("notes", JSON.stringify(notes));
}
// Render notes
function renderNotes(filter = "") {
notesContainer.innerHTML = "";
notes
.filter(note => note.text.toLowerCase().includes(filter.toLowerCase()))
.forEach((note, index) => {
const noteEl = document.createElement("div");
noteEl.classList.add("note");
const textEl = document.createElement("p");
textEl.textContent = note.text;
const actionsEl = document.createElement("div");
actionsEl.classList.add("actions");
const editBtn = document.createElement("button");
editBtn.textContent = "Edit";
editBtn.onclick = () => {
const newText = prompt("Edit note:", note.text);
if (newText !== null) {
notes[index].text = newText.trim();
saveNotes();
renderNotes(searchBox.value);
}
};
const deleteBtn = document.createElement("button");
deleteBtn.textContent = "Delete";
deleteBtn.onclick = () => {
notes.splice(index, 1);
saveNotes();
renderNotes(searchBox.value);
};
actionsEl.appendChild(editBtn);
actionsEl.appendChild(deleteBtn);
noteEl.appendChild(textEl);
noteEl.appendChild(actionsEl);
notesContainer.appendChild(noteEl);
});
}
// Add note
addNoteBtn.addEventListener("click", () => {
const text = noteText.value.trim();
if (text) {
notes.push({ text });
saveNotes();
renderNotes(searchBox.value);
noteText.value = "";
}
});
// Search notes
searchBox.addEventListener("input", (e) => {
renderNotes(e.target.value);
});
// Initial render
renderNotes();
✅ How it works:
-
Type a note and click Add Note → it shows in the grid.
-
Use Edit/Delete buttons for each note.
-
Notes are saved in localStorage → they stay even after page refresh.
-
Search box filters notes instantly.
π
Comments
Post a Comment