20 Web Dev Projects for Beginners (with Starter Code)

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.

A sensible order
StageBuildYou learn
1. StructureProfile page, recipe pageHTML elements, headings, lists, images
2. LayoutLanding page, photo gridFlexbox, grid, responsive breakpoints
3. InteractionCounter, todo list, tip calculatorDOM events, state, forms
4. DataWeather app, quiz, GitHub profile cardfetch, JSON, async/await, errors
5. ComponentsReact todo, notes app, kanbanProps, 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.

The 20 projects

Filter by level or skill, then open a project for its starter code.

Personal profile page

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>

Recipe page

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>

Product landing page

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; }

Responsive photo grid

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; }

Accessible sign-up form

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>

Click counter

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;
});

Todo list

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);
  });
});

Tip calculator

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));

Light and dark toggle

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");
});

Countdown timer

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);

Multiple choice quiz

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++;
}

Notes saved in the browser

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 }]);
}

Weather app

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 + "&current=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;
}

GitHub profile card

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";
}

Live search filter

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";
});

React counter

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>;
}

React todo app

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>
  );
}

React user directory

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>;
}

Mini kanban board

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)

Expense tracker

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;
}, {});

A blank starter file

Save this as index.html, open it in your browser, and paste any HTML project inside the body.

index.html

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>

Pick projects by the skill, not the topic

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.

Make each one presentable

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.

How it works

  1. Filter the list by level or by the skill you want to practise.
  2. Open a project to see what it teaches and its starter code.
  3. Copy the snippet into a new file, open it in a browser or a React sandbox, and get it running unchanged.
  4. Add the stretch goal listed under each project before moving to the next one.

Tips

Frequently asked questions

What should my first web dev project be?

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.

Is a todo app too basic for a portfolio?

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.

When should I learn React?

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.

Do I need a backend for these projects?

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.

How many projects do I need before applying for jobs?

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.

More in Guides

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.

Product

Who It's For

Features

Resources

Company

View Sitemap