Start with projects that teach one new idea each: a personal page for HTML and CSS, a counter and a todo list for JavaScript events and state, then a weather or quiz app for fetch and JSON. Move to React once plain JavaScript feels easy. Every project below ships with a starter snippet you can paste in and extend.
| Stage | Build | You learn |
|---|---|---|
| 1. Structure | Profile page, recipe page | HTML elements, headings, lists, images |
| 2. Layout | Landing page, photo grid | Flexbox, grid, responsive breakpoints |
| 3. Interaction | Counter, todo list, tip calculator | DOM events, state, forms |
| 4. Data | Weather app, quiz, GitHub profile card | fetch, JSON, async/await, errors |
| 5. Components | React todo, notes app, kanban | Props, state, lists, effects |
Updated
Lists of beginner projects usually stop at a title. Each project here says which skill it practises, roughly how long it takes, and gives you a few lines of working code to start from, so you spend your first hour building rather than staring at an empty file. Filter by level or by the language you want to practise.
Filter by level or skill, then open a project for its starter code.
Beginner · HTML/CSS · 1 to 2 hours. Teaches: Page structure, headings, images, links. Stretch goal: Add a dark theme with prefers-color-scheme.
<main>
<img src="me.jpg" alt="Portrait of Sam" width="160" height="160">
<h1>Sam Rivera</h1>
<p>I am learning web development and I like maps.</p>
<a href="mailto:[email protected]">Email me</a>
</main>Beginner · HTML/CSS · 1 hour. Teaches: Ordered and unordered lists, semantic sections. Stretch goal: Add a print stylesheet that hides the photo.
<article>
<h1>Lemon pancakes</h1>
<h2>Ingredients</h2>
<ul><li>2 eggs</li><li>1 cup flour</li><li>1 lemon</li></ul>
<h2>Method</h2>
<ol><li>Whisk.</li><li>Rest 10 minutes.</li><li>Fry.</li></ol>
</article>Beginner · HTML/CSS · 3 hours. Teaches: Flexbox, spacing, buttons, responsive text. Stretch goal: Make the feature row stack below 600px.
.hero { display: flex; flex-wrap: wrap; gap: 2rem; align-items: center; padding: 4rem 1rem; }
.hero h1 { font-size: clamp(2rem, 5vw, 3.5rem); }
.cta { padding: .75rem 1.5rem; border-radius: 999px; background: #4F98FB; color: #fff; }Beginner · HTML/CSS · 1 hour. Teaches: CSS grid, auto-fit, object-fit. Stretch goal: Open a larger view when a photo is clicked.
.grid { display: grid; gap: 8px; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); }
.grid img { width: 100%; aspect-ratio: 1; object-fit: cover; border-radius: 8px; }Beginner · HTML/CSS · 2 hours. Teaches: Labels, input types, built-in validation. Stretch goal: Show a custom error message under each field.
<form>
<label for="email">Email</label>
<input id="email" name="email" type="email" required>
<label for="pw">Password</label>
<input id="pw" name="pw" type="password" minlength="12" required>
<button>Create account</button>
</form>Beginner · JavaScript · 30 minutes. Teaches: Selecting elements, click events, updating text. Stretch goal: Add a reset button and stop at zero.
const btn = document.querySelector("#add");
const out = document.querySelector("#count");
let count = 0;
btn.addEventListener("click", () => {
count += 1;
out.textContent = count;
});Beginner · JavaScript · 2 hours. Teaches: Form submit, arrays, rendering a list. Stretch goal: Save todos so they survive a reload.
const todos = [];
document.querySelector("form").addEventListener("submit", (e) => {
e.preventDefault();
const input = e.target.elements.task;
todos.push(input.value.trim());
input.value = "";
document.querySelector("ul").innerHTML = "";
todos.forEach((t) => {
const li = document.createElement("li");
li.textContent = t;
document.querySelector("ul").append(li);
});
});Beginner · JavaScript · 1 hour. Teaches: Input events, numbers, formatting money. Stretch goal: Split the bill between several people.
const fmt = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" });
function update() {
const bill = Number(document.querySelector("#bill").value) || 0;
const pct = Number(document.querySelector("#pct").value) || 0;
document.querySelector("#tip").textContent = fmt.format(bill * pct / 100);
}
document.querySelectorAll("input").forEach((i) => i.addEventListener("input", update));Beginner · JavaScript · 45 minutes. Teaches: Toggling classes, saving a preference. Stretch goal: Default to the system setting on first visit.
const root = document.documentElement;
if (localStorage.getItem("theme") === "dark") root.classList.add("dark");
document.querySelector("#theme").addEventListener("click", () => {
const dark = root.classList.toggle("dark");
localStorage.setItem("theme", dark ? "dark" : "light");
});Improver · JavaScript · 1 hour. Teaches: setInterval, dates, clearing timers. Stretch goal: Let the user pick the target date.
const target = new Date("2027-01-01T00:00:00");
const timer = setInterval(() => {
const ms = target - Date.now();
if (ms <= 0) { clearInterval(timer); return; }
const days = Math.floor(ms / 86400000);
document.querySelector("#left").textContent = days + " days to go";
}, 1000);Improver · JavaScript · 3 hours. Teaches: Data in arrays of objects, scoring, state. Stretch goal: Shuffle the questions and show a progress bar.
const questions = [
{ q: "Which tag makes a link?", options: ["<a>", "<p>", "<div>"], answer: 0 },
{ q: "Which CSS property sets text colour?", options: ["font", "color", "fill"], answer: 1 },
];
let index = 0, score = 0;
function choose(i) {
if (i === questions[index].answer) score++;
index++;
}Improver · JavaScript · 2 hours. Teaches: localStorage, JSON.stringify and JSON.parse. Stretch goal: Add search and a delete button per note.
const load = () => JSON.parse(localStorage.getItem("notes") ?? "[]");
const save = (notes) => localStorage.setItem("notes", JSON.stringify(notes));
function addNote(text) {
const notes = load();
save([...notes, { id: Date.now(), text }]);
}Improver · APIs · 3 hours. Teaches: fetch, async/await, error states. Stretch goal: Show a loading message and a friendly error.
async function getWeather(lat, lon) {
const url = "https://api.open-meteo.com/v1/forecast?latitude=" + lat +
"&longitude=" + lon + "¤t=temperature_2m";
const res = await fetch(url);
if (!res.ok) throw new Error("Request failed: " + res.status);
const data = await res.json();
return data.current.temperature_2m;
}Improver · APIs · 2 hours. Teaches: Reading JSON fields, building cards from data. Stretch goal: List the user's five newest repositories.
async function card(username) {
const res = await fetch("https://api.github.com/users/" + encodeURIComponent(username));
if (!res.ok) return "User not found";
const user = await res.json();
return user.name + " has " + user.public_repos + " public repos";
}Improver · JavaScript · 1 hour. Teaches: Filtering arrays, input events, empty states. Stretch goal: Highlight the matching letters.
const items = ["Apple", "Banana", "Cherry", "Date"];
document.querySelector("#q").addEventListener("input", (e) => {
const q = e.target.value.toLowerCase();
const hits = items.filter((i) => i.toLowerCase().includes(q));
document.querySelector("#out").textContent = hits.length ? hits.join(", ") : "No matches";
});Next step · React · 30 minutes. Teaches: Components, useState, event handlers. Stretch goal: Add a step size input.
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>Clicked {count} times</button>;
}Next step · React · 3 hours. Teaches: Lists with keys, controlled inputs, immutable updates. Stretch goal: Add All, Active and Done filters.
import { useState } from "react";
export default function Todos() {
const [items, setItems] = useState([]);
const [text, setText] = useState("");
const add = (e) => {
e.preventDefault();
if (!text.trim()) return;
setItems([...items, { id: crypto.randomUUID(), text, done: false }]);
setText("");
};
return (
<form onSubmit={add}>
<label>New task <input value={text} onChange={(e) => setText(e.target.value)} /></label>
<button>Add</button>
<ul>{items.map((t) => <li key={t.id}>{t.text}</li>)}</ul>
</form>
);
}Next step · React · 2 hours. Teaches: useEffect, loading and error states. Stretch goal: Add a search box that filters by name.
import { useEffect, useState } from "react";
export default function Users() {
const [users, setUsers] = useState(null);
useEffect(() => {
fetch("/users.json").then((r) => r.json()).then(setUsers);
}, []);
if (!users) return <p>Loading...</p>;
return <ul>{users.map((u) => <li key={u.id}>{u.name}</li>)}</ul>;
}Next step · React · 4 hours. Teaches: Lifting state, moving items between lists. Stretch goal: Add drag and drop between columns.
const columns = ["Todo", "Doing", "Done"];
function move(cards, id, to) {
return cards.map((c) => (c.id === id ? { ...c, column: to } : c));
}
// render one list per column: cards.filter((c) => c.column === name)Next step · React · 4 hours. Teaches: Derived values, reduce, forms with several fields. Stretch goal: Chart spending by category.
const total = expenses.reduce((sum, e) => sum + e.amount, 0);
const byCategory = expenses.reduce((acc, e) => {
acc[e.category] = (acc[e.category] ?? 0) + e.amount;
return acc;
}, {});Save this as index.html, open it in your browser, and paste any HTML project inside the body.
Includes the viewport tag so phones show it at the right size, and a script tag for the JavaScript projects.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>My project</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- your project here -->
<script src="app.js" defer></script>
</body>
</html>
A weather app and a movie search app teach the same thing: call an API, wait for JSON, show it, handle errors. Doing both is repetition. Choose one per skill and move on.
When a project feels boring, add one requirement that forces a new idea, such as keyboard support, a loading state or a dark theme.
Give every project a readable layout, a page title, labelled form fields and visible focus outlines. Accessibility basics cost little at this size and are noticed by reviewers.
Host it free on GitHub Pages or a similar static host so anyone can click a link instead of cloning a repo.
A one-page profile about yourself in plain HTML and CSS. It needs no setup beyond a text editor and a browser, and it covers headings, paragraphs, links, images and basic styling.
On its own, yes, because almost every learner builds one. Make it yours: add due dates, filters, drag to reorder or saving to localStorage, and write a short note on what you would improve.
Once you can build the counter, todo list and a fetch-based app in plain JavaScript without a tutorial. React makes much more sense when you have felt the pain of updating the DOM by hand.
No. All twenty run in the browser. The data projects call free public APIs or a local JSON file, and the saving projects use localStorage, which keeps string data in the browser with no expiry.
There is no fixed number. Three or four finished, polished projects that show different skills usually say more than twenty half-done ones.
Related: Make a website with HTML and CSS, JavaScript event listeners, localStorage in JavaScript, JavaScript fetch and async/await, Developer portfolio examples, React Router examples.
Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.
We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.