Build an Interactive Task Tracker

Build a multi-component React task tracker with state, props, and event handling.

Introduction

30 Second Summary

Everyone has a to-do list. Whether it's on sticky notes, in a phone app, or just bouncing around your head, keeping track of tasks is something you do every day.

In this project, you will build a task tracker app using React that lets you add tasks, mark them as complete, and delete them.

What You'll Build

A task tracker application built with React and scaffolded with Vite, where you can add new tasks, mark them as complete with a strikethrough effect, and delete them.

By the end of this project, you'll have:

  • A React project scaffolded with Vite and running locally with hot module replacement.
  • A multi-component architecture with Header, TaskForm, TaskList, and Task components communicating through props.
  • Interactive features including adding, completing, and deleting tasks powered by React state and event handling.
  • Secret Mission: Add a filter bar that lets you toggle between viewing All, Active, and Completed tasks.

Are there any prerequisites?

You only need basic familiarity with HTML and JavaScript (variables, functions, arrays). No React experience is required.

Set Up Your Development Environment

React applications need a JavaScript runtime and build tools to work. Node.js provides the runtime, and Vite is a build tool that creates and serves your project during development.

In this step, you'll install Node.js, scaffold a React project with Vite, and get everything running in your browser.

In this step, get ready to:
  • Install and verify Node.js.
  • Scaffold a React project with Vite.
  • Install dependencies and start the development server.
Install Node.js

Node.js is the JavaScript runtime that powers React's build tools and package manager. Vite requires Node.js version 20.19 or higher to run.

How to open a terminal

Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.

Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.

  • Check if Node.js is already installed by running this command:
node --version

✔️ I see version 20.19 or higher

You're all set. Your Node.js version is compatible with Vite. Move on to the next substep.

ⓧ I see an older version

Your Node.js version is too old for Vite. You need version 20.19 or higher.

  • Go to the official Node.js download page: https://nodejs.org
  • Download the LTS version (this gives you a stable, long-term supported release).
  • Run the installer and follow the prompts.
  • Close and reopen your terminal, then run node --version again to confirm it shows 20.19 or higher.

Still seeing the old version?

On macOS, the system may still point to an older Node.js. Try running which node to check which installation your terminal is using. You may need to update your shell's PATH or use a version manager like nvm.

Help me fix my Node.js version

ⓧ Command not found

Node.js isn't installed on your machine yet.

  • Go to the official Node.js download page: https://nodejs.org
  • Download the LTS version (this gives you a stable, long-term supported release).
  • Run the installer and follow the prompts. Accept the defaults.
  • Close and reopen your terminal, then run node --version to confirm it shows a version number starting with 20 or higher.

Still getting command not found?

Make sure you fully closed and reopened your terminal after installation. If it still doesn't work, restart your computer and try again.

Help me troubleshoot Node.js installation

Scaffold your React project with Vite

Vite's scaffolding tool creates a ready-to-go project structure with all the configuration files you need. You'll run a single command that generates your entire project folder.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your React project by running this command:
npm create vite@latest task-tracker -- --template react

What does this command do?

  • npm create vite@latest runs the latest version of Vite's project scaffolding tool.
  • task-tracker is the name of your project folder.
  • -- --template react tells Vite to use the React template with JavaScript.

Why Vite instead of Create React App?

Create React App was the old standard for starting React projects, but it's now officially deprecated.

Vite is faster, simpler, and recommended for learning React's core concepts without extra complexity.

  • Confirm the project folder was created by running:
ls

You should see a task-tracker folder in the output.

Don't see the task-tracker folder?

Make sure you ran the scaffold command from your Desktop. Run pwd to check your current location.

Help me find my project folder

Install dependencies and start the dev server

Your project folder has a package.json file that lists the packages it needs (React, Vite, etc.). You need to download these packages before the app can run.

  • Navigate into your project folder and install its dependencies by running these commands:
cd task-tracker
npm install

What does npm install do?

npm install reads package.json and downloads every listed dependency into a node_modules folder. This is where React and Vite's code lives on your machine.

  • Start the development server by running:
npm run dev
  • You should see output in your terminal with a local URL. Open http://localhost:5173 in your browser.

You should see the default Vite + React welcome page with a spinning logo and counter button.

Don't see the page?

Make sure you're inside the task-tracker folder. Run pwd to check.

Confirm that npm install completed without errors before running npm run dev.

Help me debug the dev server

Now open VS Code so you can start editing files.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type VS Code and press Enter to open it.
  • Click File in the top menu bar.
  • Click Open Folder.
  • Select the task-tracker folder on your Desktop, then click Open.

What about the terminal?

Your dev server is still running in the original terminal. Leave it running. You can use VS Code's built-in terminal for any additional commands by pressing Ctrl+` (both macOS and Windows).

Now customize the project by updating the page title and cleaning up the default styles.

  • In VS Code's Explorer sidebar on the left, click index.html to open it.
  • Find this line:
<title>Vite + React</title>
  • Replace Vite + React with Task Tracker, so the line looks like this:
<title>Task Tracker</title>
  • Save index.html.

✔️ Awesome, I've got everything!

Great. Double check you've saved the file and move on.

ⓧ I'd like to double check the full code

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Task Tracker</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>

Next, replace the default styles with a clean starting point for your app.

  • In the Explorer sidebar, expand the src folder and click index.css to open it.
  • Select all the existing content (Cmd+A on macOS or Ctrl+A on Windows) and replace it with this code:
:root {
  font-family: system-ui, -apple-system, sans-serif;
  line-height: 1.5;
  font-weight: 400;
  color: #213547;
  background-color: #ffffff;
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  min-height: 100vh;
}

What does this CSS do?

  • :root sets a clean system font, light background, and dark text color for the whole page.
  • box-sizing: border-box makes padding and borders count inside element widths, which prevents layout surprises.
  • body removes default margins and ensures the page fills at least the full viewport height.
  • Save src/index.css.
  • Check your browser. The page still loads, but the Vite default styling is gone. You should see a plain white background with the React logo and counter button unstyled.

Page looks broken or blank?

Make sure you replaced ALL the content in src/index.css, not just part of it. The old file had many CSS variables and dark mode styles that should be completely removed.

Check that your dev server is still running in the terminal. If it stopped, run npm run dev again.

Help me fix my page styling

✔️ Awesome, I've got everything!

Great. Double check you've saved the file and move on.

ⓧ I'd like to double check the full code

:root {
  font-family: system-ui, -apple-system, sans-serif;
  line-height: 1.5;
  font-weight: 400;
  color: #213547;
  background-color: #ffffff;
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  min-height: 100vh;
}

Your development environment is ready and your project is running in the browser. Next up, you'll create your first custom React component.

Create the Header Component

Your development environment is ready and the Vite dev server is running. Now it's time to build your first React component.

Each component handles one piece of the UI. You'll create a Header component that displays the app title and a live count of how many tasks exist.

In this step, get ready to:
  • Create a Header component that accepts props.
  • Replace the default scaffold content in App.jsx.
  • Style the header with CSS.
Create the Header component file

React apps are organized by splitting each piece of UI into its own file. Your Header will live in a dedicated components folder inside src.

  • In VS Code's Explorer sidebar, right-click the src folder.
  • Select New Folder.
  • Name it components.
  • Right-click the new components folder.
  • Select New File.
  • Name it Header.jsx.

You should see Header.jsx listed under src/components in the Explorer sidebar.

  • Add the following code to Header.jsx:
function Header({ taskCount }) {
  return (
    <header className="header">
      <h1>Task Tracker</h1>
      <p className="task-count">{taskCount} {taskCount === 1 ? 'task' : 'tasks'}</p>
    </header>
  )
}

export default Header

What does this code do?

  • { taskCount } in the function parameter destructures the props object to grab the taskCount value passed from the parent component.
  • The ternary taskCount === 1 ? 'task' : 'tasks' displays correct singular or plural text depending on the count.
  • export default Header makes this component available for other files to import.
  • Save Header.jsx.

You won't see a change in the browser yet. The next substep imports this component into App.jsx.

Seeing a red squiggly in the editor?

If VS Code shows a warning on the JSX syntax, that's normal until the file is saved and recognized as a React component. Make sure your file ends in .jsx (not .js).

help me fix a JSX syntax warning in my Header.jsx file

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

function Header({ taskCount }) {
  return (
    <header className="header">
      <h1>Task Tracker</h1>
      <p className="task-count">{taskCount} {taskCount === 1 ? 'task' : 'tasks'}</p>
    </header>
  )
}

export default Header
Import and render the Header in App.jsx

The default App.jsx contains Vite's starter content. You'll replace it with a clean app shell that imports your Header and sets up state to track tasks.

  • Open src/App.jsx in the editor.
  • Select all the existing code (Cmd+A on macOS or Ctrl+A on Windows) and replace it with the following:
import { useState } from 'react'
import Header from './components/Header.jsx'
import './App.css'

function App() {
  // Create a state variable to hold our tasks array
  const [tasks, setTasks] = useState([])

  return (
    <div className="app">
      <Header taskCount={tasks.length} />
    </div>
  )
}

export default App

What does this code do?

  • import { useState } from 'react' brings in React's state hook, which lets your component remember data between renders.
  • const [tasks, setTasks] = useState([]) creates an empty array called tasks and a function setTasks to update it. When you call setTasks, React re-renders the component with the new data.
  • <Header taskCount={tasks.length} /> renders your Header and passes the current number of tasks as a prop. Right now tasks.length is 0 because the array is empty.

What is a prop?

Props are how parent components pass data down to child components. Think of them like function arguments. Here, App passes taskCount to Header, and Header uses it to display the count.

  • Save App.jsx.
  • Check your browser. You should see "Task Tracker" as a heading and "0 tasks" below it.

Don't see the header in your browser?

  • Make sure you saved both Header.jsx and App.jsx.
  • Check that the import path matches exactly: './components/Header.jsx'.
  • Look at your terminal for error messages. A red error overlay in the browser means there's a syntax issue in one of your files.

help me debug why my Header component isn't showing up

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

import { useState } from 'react'
import Header from './components/Header.jsx'
import './App.css'

function App() {
  const [tasks, setTasks] = useState([])

  return (
    <div className="app">
      <Header taskCount={tasks.length} />
    </div>
  )
}

export default App
Add CSS styling for the header

The header is rendering but has no custom styles. You'll replace the default CSS with styles that center the layout and polish the typography.

  • Open src/App.css in the editor.
  • Select all the existing content (Cmd+A on macOS or Ctrl+A on Windows) and replace it with the following:
/* Center the app and set a max width */
.app {
  max-width: 500px;
  margin: 2rem auto;
  padding: 0 1rem;
  font-family: system-ui, -apple-system, sans-serif;
}

/* Center the header text */
.header {
  text-align: center;
  margin-bottom: 1.5rem;
}

/* Remove default margin from the title */
.header h1 {
  margin: 0;
  color: #1a1a2e;
}

/* Style the task count in a lighter color */
.task-count {
  color: #666;
  margin: 0.25rem 0 0;
}

What does this CSS do?

  • .app centers the entire app on the page with a max width of 500px.
  • .header centers the text and adds spacing below the header.
  • .header h1 removes the default browser margin from the heading and sets a dark color.
  • .task-count gives the count a lighter gray color so it reads as secondary information.
  • Save App.css.
  • Check your browser. "Task Tracker" should now be centered on the page with "0 tasks" in gray text below it.

Styles not showing up?

  • Confirm you saved App.css (not index.css).
  • Make sure className="app" in your App.jsx matches the .app selector in your CSS.
  • Try a hard refresh with Cmd+Shift+R (macOS) or Ctrl+Shift+R (Windows).

help me figure out why my CSS styles aren't applying

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

.app {
  max-width: 500px;
  margin: 2rem auto;
  padding: 0 1rem;
  font-family: system-ui, -apple-system, sans-serif;
}

.header {
  text-align: center;
  margin-bottom: 1.5rem;
}

.header h1 {
  margin: 0;
  color: #1a1a2e;
}

.task-count {
  color: #666;
  margin: 0.25rem 0 0;
}

Your first component is rendering and styled. Next up, you'll build a form that lets you type in and add new tasks.

Build the Task Form Component

Your Header component is displaying a task count, but right now that count is stuck at zero.

In this step, you'll build a form component that lets users type in new tasks and add them to the list. You'll also learn how React components communicate with each other by passing data upward through callback props.

In this step, get ready to:
  • Create a TaskForm component with a controlled text input.
  • Wire up the form to add new task objects to App state.
  • Test adding tasks and watch the header count update.
Create the TaskForm component

In React, a "controlled input" is a form field whose value is driven by component state. Every keystroke updates the state, and the state feeds back into the input's displayed value.

  • Create a new file called src/components/TaskForm.jsx and add the following code:
import { useState } from 'react'

function TaskForm({ onAddTask }) {
  // Track the current value of the text input
  const [text, setText] = useState('')

  function handleSubmit(e) {
    // Prevent the browser from refreshing the page
    e.preventDefault()
    // Ignore empty submissions
    if (text.trim() === '') return
    // Send the trimmed text up to the parent component
    onAddTask(text.trim())
    // Clear the input field after adding
    setText('')
  }

  return (
    <form className="task-form" onSubmit={handleSubmit}>
      <input
        type="text"
        placeholder="Add a new task..."
        value={text}
        onChange={(e) => setText(e.target.value)}
      />
      <button type="submit">Add</button>
    </form>
  )
}

export default TaskForm

What does this code do?

The text state variable holds whatever the user has typed so far. The onChange handler updates this state on every keystroke, keeping the input and state perfectly in sync.

The handleSubmit function runs when the form is submitted. It calls e.preventDefault() to stop the browser from refreshing the page, sends the text up to the parent via the onAddTask prop, then clears the input.

  • Save TaskForm.jsx.

Why does onAddTask come from a prop?

TaskForm doesn't own the task list. It only knows how to collect user input. The parent component (App) owns the list and passes a callback function down as a prop.

This pattern keeps each component focused on one job. The form handles input. The parent handles data.

Connect TaskForm to App state

TaskForm expects an onAddTask prop, so you need to define that function in App.jsx and pass it down.

  • Replace the entire contents of src/App.jsx with the following code:
import { useState } from 'react'
import Header from './components/Header.jsx'
import TaskForm from './components/TaskForm.jsx'
import './App.css'

function App() {
  const [tasks, setTasks] = useState([])

  // Create a new task object and append it to the tasks array
  function handleAddTask(text) {
    const newTask = {
      id: Date.now(),
      text: text,
      completed: false,
    }
    setTasks([...tasks, newTask])
  }

  return (
    <div className="app">
      <Header taskCount={tasks.length} />
      <TaskForm onAddTask={handleAddTask} />
    </div>
  )
}

export default App

What does this code do?

The handleAddTask function creates a task object with three properties: id (a unique timestamp), text (what the user typed), and completed (starts as false).

The spread syntax [...tasks, newTask] creates a brand new array containing all existing tasks plus the new one. React detects the new array and re-renders the component.

  • Save App.jsx and check your browser.
  • You should see a text input field and an Add button below the header.
  • Type a task name and click Add (or press Enter).
  • The header count should change from "0 tasks" to "1 task".

Header count not updating?

  • Make sure you imported TaskForm at the top of App.jsx and rendered <TaskForm onAddTask={handleAddTask} /> inside the return.
  • Check that handleAddTask is defined inside the App function, not outside it. It needs access to setTasks.
  • Confirm the prop name matches exactly: onAddTask in App.jsx must match onAddTask in the TaskForm destructuring.

Help me debug why my task form isn't adding tasks to the header count.

✔️ Awesome, I've got everything!

Great. Double check you've saved both files before moving on.

ⓧ I'd like to double check the full code

Here is src/components/TaskForm.jsx:

import { useState } from 'react'

function TaskForm({ onAddTask }) {
  const [text, setText] = useState('')

  function handleSubmit(e) {
    e.preventDefault()
    if (text.trim() === '') return
    onAddTask(text.trim())
    setText('')
  }

  return (
    <form className="task-form" onSubmit={handleSubmit}>
      <input
        type="text"
        placeholder="Add a new task..."
        value={text}
        onChange={(e) => setText(e.target.value)}
      />
      <button type="submit">Add</button>
    </form>
  )
}

export default TaskForm

Here is src/App.jsx:

import { useState } from 'react'
import Header from './components/Header.jsx'
import TaskForm from './components/TaskForm.jsx'
import './App.css'

function App() {
  const [tasks, setTasks] = useState([])

  function handleAddTask(text) {
    const newTask = {
      id: Date.now(),
      text: text,
      completed: false,
    }
    setTasks([...tasks, newTask])
  }

  return (
    <div className="app">
      <Header taskCount={tasks.length} />
      <TaskForm onAddTask={handleAddTask} />
    </div>
  )
}

export default App

Your task tracker can now accept user input and store tasks in state. Next up, you'll build the components that actually display those tasks on screen.

Display Tasks with TaskList and Task Components

Your form can add tasks to the state array, but nothing shows up on screen yet. The tasks exist in memory with no visual representation.

In this step, you'll build two components that work together to display tasks. A Task component renders a single item, and a TaskList component orchestrates the full list. You'll also wire up toggle and delete handlers so users can interact with their tasks.

In this step, get ready to:
  • Create a Task component that renders a single task with toggle and delete actions.
  • Create a TaskList component that maps over tasks and renders Task components.
  • Wire up toggle and delete handlers in App so tasks are fully interactive.
Create the Task component

Each task in the list needs its own component that knows how to display itself and respond to user clicks. The Task component will receive the task data plus callback functions for toggling and deleting.

  • Create a new file called Task.jsx inside the src/components/ folder.
  • Add the following code to Task.jsx:
function Task({ task, onToggle, onDelete }) {
  return (
    <li className={`task ${task.completed ? 'completed' : ''}`}>
      <span className="task-text" onClick={onToggle}>
        {task.text}
      </span>
      <button className="delete-btn" onClick={onDelete}>
        Delete
      </button>
    </li>
  )
}

export default Task

What does this code do?

This component receives three props: the task object itself, an onToggle callback, and an onDelete callback.

  • The className uses a template literal to add a completed class when the task is done. This is how you'll apply strikethrough styling later.
  • Clicking the task text calls onToggle, which flips the completed status.
  • The Delete button calls onDelete to remove the task from the list entirely.
  • Save Task.jsx.

You won't see a change yet because nothing renders this component. The next chunk connects it.

Create the TaskList component

The TaskList component is responsible for looping through all the tasks and rendering a Task component for each one. It also handles the empty state when no tasks exist.

  • Create a new file called TaskList.jsx inside the src/components/ folder.
  • Add the following code to TaskList.jsx:
import Task from './Task.jsx'

function TaskList({ tasks, onToggleTask, onDeleteTask }) {
  // Show a message when there are no tasks
  if (tasks.length === 0) {
    return <p className="empty-message">No tasks yet. Add one above!</p>
  }

  return (
    <ul className="task-list">
      {tasks.map((task) => (
        <Task
          key={task.id}
          task={task}
          onToggle={() => onToggleTask(task.id)}
          onDelete={() => onDeleteTask(task.id)}
        />
      ))}
    </ul>
  )
}

export default TaskList

What does this code do?

  • The early return checks if the tasks array is empty and shows a friendly message instead of a blank space.
  • The .map() method loops through each task object and returns a <Task> component for it.
  • The key={task.id} prop helps React track which items changed, were added, or were removed. Every list item in React needs a unique key.
  • Arrow functions wrap onToggleTask(task.id) and onDeleteTask(task.id) so the correct task ID is passed when the user clicks.

Why does each Task need a key?

When you render a list in React, it needs a way to tell items apart. The key prop gives each element a stable identity so React can efficiently update only the items that actually changed, rather than re-rendering the entire list.

  • Save TaskList.jsx.

You won't see a change in the browser yet. The TaskList component exists but App doesn't render it.

Seeing a red error in your browser?

If you see an import error, make sure the file is named exactly TaskList.jsx (capital T, capital L) and saved inside src/components/.

Also confirm that the import path at the top of TaskList says ./Task.jsx with a capital T.

help me fix an import error in my TaskList component

Wire up toggle and delete in App

The Task component fires callbacks when clicked, but those callbacks need to live in App where the state is. You'll add two handler functions and pass them down through TaskList.

  • Open src/App.jsx in VS Code.
  • Find this import line near the top of the file:
import TaskForm from './components/TaskForm.jsx'
  • Add a new import for TaskList directly below it:
import TaskList from './components/TaskList.jsx'
  • Now find the closing brace of the handleAddTask function. Add the following two handler functions below it:
  function handleToggleTask(id) {
    setTasks(
      tasks.map((task) =>
        task.id === id ? { ...task, completed: !task.completed } : task
      )
    )
  }

  function handleDeleteTask(id) {
    setTasks(tasks.filter((task) => task.id !== id))
  }

What do these handlers do?

  • handleToggleTask uses .map() to create a new array where only the matching task has its completed value flipped. The spread operator ...task copies the original task and overrides just the one property.
  • handleDeleteTask uses .filter() to create a new array that excludes the task with the matching id. React sees the new array and re-renders the list without that item.
  • Finally, find this line in the return statement:
      <TaskForm onAddTask={handleAddTask} />
  • Add the TaskList component directly below it, passing in your tasks and handlers:
      <TaskList
        tasks={tasks}
        onToggleTask={handleToggleTask}
        onDeleteTask={handleDeleteTask}
      />
  • Save App.jsx and check your browser.

You should see the message "No tasks yet. Add one above!" below the form. Type a task name and click Add. The task appears in the list with a Delete button next to it.

  • Click the task text to toggle it between active and completed.
  • Click Delete to remove the task.
  • Confirm the header count updates as you add and remove tasks.

Tasks not appearing when you click Add?

Make sure the <TaskList> component is placed inside the <div className="app"> wrapper in your return statement, below <TaskForm />.

Check that you're passing tasks={tasks} as a prop. Without it, TaskList receives an empty array.

help me debug why tasks aren't showing in my TaskList

✔️ Awesome, I've got everything!

Great. Double check that you've saved all three files.

ⓧ I'd like to double check the full code

Here's the complete code for each file at this point.

function Task({ task, onToggle, onDelete }) {
  return (
    <li className={`task ${task.completed ? 'completed' : ''}`}>
      <span className="task-text" onClick={onToggle}>
        {task.text}
      </span>
      <button className="delete-btn" onClick={onDelete}>
        Delete
      </button>
    </li>
  )
}

export default Task
import Task from './Task.jsx'

function TaskList({ tasks, onToggleTask, onDeleteTask }) {
  if (tasks.length === 0) {
    return <p className="empty-message">No tasks yet. Add one above!</p>
  }

  return (
    <ul className="task-list">
      {tasks.map((task) => (
        <Task
          key={task.id}
          task={task}
          onToggle={() => onToggleTask(task.id)}
          onDelete={() => onDeleteTask(task.id)}
        />
      ))}
    </ul>
  )
}

export default TaskList
import { useState } from 'react'
import Header from './components/Header.jsx'
import TaskForm from './components/TaskForm.jsx'
import TaskList from './components/TaskList.jsx'
import './App.css'

function App() {
  const [tasks, setTasks] = useState([])

  function handleAddTask(text) {
    const newTask = {
      id: Date.now(),
      text: text,
      completed: false,
    }
    setTasks([...tasks, newTask])
  }

  function handleToggleTask(id) {
    setTasks(
      tasks.map((task) =>
        task.id === id ? { ...task, completed: !task.completed } : task
      )
    )
  }

  function handleDeleteTask(id) {
    setTasks(tasks.filter((task) => task.id !== id))
  }

  return (
    <div className="app">
      <Header taskCount={tasks.length} />
      <TaskForm onAddTask={handleAddTask} />
      <TaskList
        tasks={tasks}
        onToggleTask={handleToggleTask}
        onDeleteTask={handleDeleteTask}
      />
    </div>
  )
}

export default App

Your task tracker is fully functional. You can add, toggle, and delete tasks. Next up, you'll add CSS to make it look polished and give completed tasks a visual strikethrough effect.

Style Your App

Your task tracker is fully functional. You can add tasks, mark them as complete, and delete them. The logic is solid, but the interface still looks bare.

In this step, you'll add CSS to make the app look polished. You'll style the form, the task list, and add a satisfying strikethrough effect on completed tasks.

In this step, get ready to:
  • Style the task form with a modern input and button layout.
  • Style the task list and individual task items.
  • Test the full app flow from adding to completing to deleting tasks.
Style the task form

Right now the form input and button are unstyled browser defaults. You'll turn them into a clean side-by-side layout with a purple accent color.

  • In src/App.css, find the closing brace of the .task-count rule. Add the following styles below it:
/* Form layout - input and button side by side */
.task-form {
  display: flex;
  gap: 0.5rem;
  margin-bottom: 1.5rem;
}

/* Text input stretches to fill available space */
.task-form input {
  flex: 1;
  padding: 0.6rem 1rem;
  border: 2px solid #e0e0e0;
  border-radius: 6px;
  font-size: 1rem;
}

/* Highlight the input border on focus */
.task-form input:focus {
  outline: none;
  border-color: #646cff;
}

/* Submit button with purple accent */
.task-form button {
  padding: 0.6rem 1.2rem;
  background: #646cff;
  color: white;
  border: none;
  border-radius: 6px;
  font-size: 1rem;
  cursor: pointer;
}

/* Darken on hover for feedback */
.task-form button:hover {
  background: #535bf2;
}

What does this CSS do?

  • display: flex places the input and button on the same row.
  • flex: 1 on the input makes it stretch to fill the remaining space after the button.
  • border-color: #646cff on focus gives the learner a clear visual signal that the input is active.
  • Save src/App.css.
  • Check your browser. You should see the input field and the purple Add button sitting side by side with rounded corners.

Form still looks unstyled?

  • Make sure you placed the new CSS below the .task-count block, not inside it.
  • Check that your TaskForm.jsx uses className="task-form" on the <form> element.
  • Confirm you saved the file. Vite hot-reloads on save, but unsaved changes won't appear.

Why isn't my task form CSS showing up?

Style the task list and completed tasks

Each task needs to look like a distinct card with space between the text and the delete button. Completed tasks should visually fade out with a strikethrough to signal they're done.

  • In src/App.css, add the following styles below the .task-form button:hover rule you just added:
/* Remove default list bullets and spacing */
.task-list {
  list-style: none;
  padding: 0;
  margin: 0;
}

/* Each task is a row with space between text and button */
.task {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.75rem 1rem;
  margin-bottom: 0.5rem;
  background: #f8f9fa;
  border-radius: 6px;
  border: 1px solid #e0e0e0;
}

/* Completed tasks get a strikethrough and fade out */
.task.completed .task-text {
  text-decoration: line-through;
  opacity: 0.5;
}

/* Task text is clickable and fills available space */
.task-text {
  cursor: pointer;
  flex: 1;
}

/* Red delete button */
.delete-btn {
  padding: 0.3rem 0.7rem;
  background: #ff4757;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 0.85rem;
}

/* Darken on hover */
.delete-btn:hover {
  background: #e84141;
}

/* Message shown when no tasks exist */
.empty-message {
  text-align: center;
  color: #999;
  font-style: italic;
}

What does this CSS do?

  • .task uses flexbox to push the task text and delete button to opposite ends of the row.
  • .task.completed .task-text applies a strikethrough and 50% opacity. This selector only activates when React adds the completed class to the parent <li>.
  • flex: 1 on .task-text ensures the text fills all available space, pushing the delete button to the right.

How does React control the strikethrough?

In your Task.jsx component, the className uses a template literal: `task ${task.completed ? 'completed' : ''}`. When the user clicks a task, React toggles task.completed in state, which adds or removes the completed class.

Your CSS just sits there waiting. The moment the class appears, the strikethrough and opacity kick in automatically.

  • Save src/App.css.
  • In your browser, type a task name in the input and click Add. You should see a light grey card with the task text on the left and a red Delete button on the right.

Tasks showing as plain text with bullets?

  • Check that TaskList.jsx uses className="task-list" on the <ul> element. Without it, the browser's default list styles still apply.
  • Make sure Task.jsx uses className="delete-btn" on the delete button. A typo means the red styling won't match.

Help me debug why my task list CSS isn't applying.

Test the full app flow

Your styling is complete. Time to run through every feature and confirm the visuals match the behavior.

  • In your browser, add three tasks by typing a name and clicking Add each time.
  • Confirm the header shows 3 tasks.
  • Click on one of the task texts. The text should get a strikethrough and fade to 50% opacity.
  • Click the red Delete button on another task. It should disappear and the header count should drop to 2 tasks.
  • Delete all remaining tasks. You should see the italic message No tasks yet. Add one above! in grey.

Click the text, not the row

The toggle is on the .task-text span, not the entire row. Clicking the grey background area between the text and the delete button won't trigger the completed state.

✔️ Awesome, I've got everything!

Great. Double check you've saved src/App.css and all your task interactions are working.

ⓧ I'd like to double check the full code

.app {
  max-width: 500px;
  margin: 2rem auto;
  padding: 0 1rem;
  font-family: system-ui, -apple-system, sans-serif;
}

.header {
  text-align: center;
  margin-bottom: 1.5rem;
}

.header h1 {
  margin: 0;
  color: #1a1a2e;
}

.task-count {
  color: #666;
  margin: 0.25rem 0 0;
}

.task-form {
  display: flex;
  gap: 0.5rem;
  margin-bottom: 1.5rem;
}

.task-form input {
  flex: 1;
  padding: 0.6rem 1rem;
  border: 2px solid #e0e0e0;
  border-radius: 6px;
  font-size: 1rem;
}

.task-form input:focus {
  outline: none;
  border-color: #646cff;
}

.task-form button {
  padding: 0.6rem 1.2rem;
  background: #646cff;
  color: white;
  border: none;
  border-radius: 6px;
  font-size: 1rem;
  cursor: pointer;
}

.task-form button:hover {
  background: #535bf2;
}

.task-list {
  list-style: none;
  padding: 0;
  margin: 0;
}

.task {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.75rem 1rem;
  margin-bottom: 0.5rem;
  background: #f8f9fa;
  border-radius: 6px;
  border: 1px solid #e0e0e0;
}

.task.completed .task-text {
  text-decoration: line-through;
  opacity: 0.5;
}

.task-text {
  cursor: pointer;
  flex: 1;
}

.delete-btn {
  padding: 0.3rem 0.7rem;
  background: #ff4757;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 0.85rem;
}

.delete-btn:hover {
  background: #e84141;
}

.empty-message {
  text-align: center;
  color: #999;
  font-style: italic;
}

Your task tracker is fully styled and functional. Next up, you'll take on the Secret Mission and add task filtering to your app.

Secret mission

Add Task Filtering

Your task tracker works great, but what happens when you have dozens of tasks? Right now, completed tasks mix in with active ones. Add a filter bar that lets you toggle between viewing All, Active, and Completed tasks.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • The task-tracker project folder on your Desktop.
  • The Vite dev server running in your terminal.

Keep everything running

No action needed. Choose this if you're still actively building or want to keep experimenting with your task tracker.

  • Your dev server stays running at http://localhost:5173 and hot-reloads any changes you make.
  • Your task-tracker folder stays on your Desktop.

Pause - I'll come back to this later

Shut down the dev server to free up your terminal. Your project files stay on disk so you can pick up where you left off.

  • In the terminal where the dev server is running, press Ctrl+C to stop the server.
  • To restart later, open a terminal in your task-tracker folder and run npm run dev.

Delete - I don't want to use this again

Remove all project files and start fresh. This is permanent.

  • In the terminal where the dev server is running, press Ctrl+C to stop the server.
  • Delete the project folder by running the following command for your operating system.

macOS

rm -rf ~/Desktop/task-tracker

Windows

Remove-Item -Recurse -Force ~\Desktop\task-tracker

Nice Work!

Nice Work!

You just built your first React app from scratch. You now have a fully interactive task tracker running in your browser, complete with components, state management, and dynamic rendering.

You've learned how to:

  • Built a multi-component React application scaffolded with Vite.
  • Managed interactive data with the useState hook and passed data between components using props and callback functions.
  • Rendered dynamic lists with .map() and keys, and applied conditional styling based on state.
  • Added filtering logic to toggle between All, Active, and Completed task views.

Ready to quiz yourself?