Skip to content

Practice: Build an interactive web application

Build an independent task-board application with HTML, CSS, and JavaScript. A user can add a task, see every task rendered from application state, and change a task’s completion state without a page reload.

This is Stage 1 of one continuing source-code project. Preserve the repository after this assignment. You will adapt the same application with responsive components and accessible motion in the Responsive design and animation assignment.

What you will practice

  • Connect form and checkbox events to deliberate application behavior.
  • Store task records in one JavaScript state object and render the interface from that state.
  • Validate user input and preserve useful keyboard focus through state changes.
  • Use Git checkpoints and browser tests to show how the application reached its final state.

Many interfaces use the same cycle: an event occurs, application state changes, and the interface renders the new state. The product can be a task board, booking view, settings panel, or shopping basket. The names change, but the technical relationship remains useful.

  • New: You will design the complete DOM contract and implement the event-state-render cycle without copying the lesson project.
  • Reused: External JavaScript, defer, values, functions, conditions, arrays, objects, DOM selection, form events, checkbox events, textContent, state updates, rendering, keyboard tests, and the solo Git workflow.
  • Reserved for Stage 2: Container queries, component-level responsive changes, transitions, keyframe animation, and reduced-motion behavior.

Starting point

Before you start

  • Complete the JavaScript foundations, DOM events, and interface-state lessons.
  • Use VS Code, a current browser with developer tools, Git, and a private GitHub repository.
  • Start from a new folder. Do not reuse the lesson repository or its Git history.
  • Keep all names, example records, and test data fictional. Do not store personal or sensitive information.
Current state
You have a working lesson task tracker, but you do not yet have an independent Level 2 application repository.
First action
Create a folder named level-2-task-board and add empty index.html, styles.css, app.js, and README.md files.
First checkpoint
The new page loads its own stylesheet and JavaScript file, shows the chosen product heading, and has no Console errors.
Help trigger
Use the checkpoint assistance or ask for help when the first Console error remains unresolved after you read its file, line, and error type.

Requirements

The required assignment is complete when every applicable criterion below is met.

Required behavior

  • A form contains one visible task-title label, one task-title text field, one visible category label, one category select, and one submit button.
  • Submitting an empty or whitespace-only title shows one specific error, sets aria-invalid on the title field, moves focus to that field, and does not change application state.
  • Submitting valid input adds one task object to application state with a unique ID, the submitted title, the selected category, and completed set to false.
  • Every valid submission renders the complete task list from state, clears the form, and returns focus to the title field.
  • Each rendered task has a native checkbox. Changing the checkbox updates the matching task object before the interface renders again.
  • A visible summary always reports the correct number of completed tasks and total tasks.
  • A status message identifies the task that was added or changed. The result appears without a page reload.
  • Reloading the page returns the documented initial state. Persistence is not required in this stage.

Semantic HTML and accessibility

  • The page uses appropriate header, main, section, form, label, button, list, and heading elements.
  • Every form control has a persistent visible label, and instructions or errors are programmatically connected to the title field.
  • All required behavior works with the keyboard. Focus remains visible, and a re-render does not move focus to the page start.
  • The status output uses role="status" or an equivalent polite live-region pattern.
  • Task titles entered by a user are inserted with textContent or an equivalent safe text API, not innerHTML.
  • The base interface remains readable without horizontal page scrolling at a 320 CSS-pixel viewport and at 200% browser zoom.

Technical constraints

  • Use index.html, styles.css, and app.js as separate files. Load app.js with defer and use strict mode.
  • Keep one state object as the source of truth for task records. Derive the visible list and summary from that state.
  • Register event listeners with addEventListener. Do not use inline JavaScript attributes.
  • Use an event → state update → render sequence for both task creation and completion changes.
  • Use browser-native HTML, CSS, and JavaScript. Do not use a framework, component library, or generated application code for the required path.
  • Use one dedicated Git repository and one private GitHub repository. Record each passing checkpoint with a focused commit.

Code quality

  • Use specific names for selected elements, state values, event handlers, and render functions.
  • Keep stable DOM selections and listener registration outside the render function.
  • Remove temporary logs, unused variables, commented-out code, and unresolved Console errors before submission.
  • Use comments only to explain intent or a non-obvious constraint.

Design freedom

  • Choose one brief below and write original fictional task content for it.
  • Choose the product name, categories, typeface, color palette, spacing scale, and exact visual treatment.
  • Choose whether the completion control appears before or after the task text, provided its label remains clear.

Out of scope

  • A login, database, backend, public deployment, API, drag-and-drop interaction, and multi-user synchronization are not required.
  • Task removal, filtering, editing, ordering, due dates, and localStorage are not required.
  • Advanced responsive component changes and animation are not required until Stage 2.
  • Real names, school information, health information, credentials, analytics, and other personal data must not enter the application or repository.

The required assignment is complete when every requirement applies to one identified commit, the test sequence in the self-check passes, the same commit exists on the private remote, and git status reports a clean working tree. Optional extensions do not change this finish line.

Each brief uses the same technical contract. Choose the context that gives you the clearest task titles and categories.

Build a board for a fictional small web project. Use categories such as Content, Design, Development, and Review. Example tasks can include reviewing navigation labels or testing a form.

Build a board for a fictional technical workshop. Use categories such as Room, Equipment, Material, and Communication. Keep participant and staff information fictional.

Build a board for a fictional public event. Use categories such as Venue, Program, Accessibility, and Promotion. Do not include real attendee, volunteer, or contact information.

Write the selected brief, intended user, and chosen categories in README.md. Keep the core data model stable across the three briefs:

Required task record shape
const task = {
id: 1,
title: "Test the registration form",
category: "Review",
completed: false,
};

The values can differ. The four property roles must remain clear so the later responsive, testing, SEO, and analytics work can use one stable product model.

Checkpoint 1: Open an independent project baseline

Section titled “Checkpoint 1: Open an independent project baseline”

Create this visible structure:

Project files
level-2-task-board/
├── index.html
├── styles.css
├── app.js
└── README.md

In index.html, create a valid document with a descriptive page title. Link styles.css, and load app.js with defer. Add a header, a main region, and the product’s main heading. Keep this checkpoint small. The form and application state belong to later checkpoints.

In app.js, add strict mode and one temporary startup message:

app.js baseline
"use strict";
console.log("Task board JavaScript loaded.");

Open the page. Confirm the heading appears, the stylesheet request succeeds, and the Console shows the startup message once. Remove the temporary log after the check.

Open a terminal in level-2-task-board, then run:

Create the local repository
git init
git add index.html styles.css app.js README.md
git commit -m "Create task board baseline"
git status

Create a private GitHub repository named level-2-task-board. Connect the local repository by following the empty repository’s push an existing repository instructions. Push the baseline, then confirm that the four files appear on GitHub and the repository is private.

Do not copy the hidden .git folder from a lesson project. A copied .git folder would mix the new product with the lesson history and remote.

Reload the page with the Console open. The main heading must appear, the Console must have no errors, and git status must report a clean working tree.

Assistance 1 — Confirm the active folder and loaded files

In VS Code, confirm that the Explorer shows index.html, styles.css, app.js, and README.md directly inside level-2-task-board. In browser developer tools, use the Network or Sources panel to confirm that the loaded app.js comes from this folder.

Assistance 2 — Trace a missing stylesheet or script

Compare each href and src value with the exact filename in the Explorer. A file in the same folder uses styles.css or app.js, without a leading slash. Confirm that the script element includes defer.

Checkpoint: Independent baseline is recorded

What now works
The new HTML page loads its own CSS and JavaScript without a Console error, and the private remote contains the baseline commit.
Files changed
index.html, styles.css, app.js, README.md
What remains
Build the semantic form, validation path, application state, and rendered task list.
Next action
Open index.html and add the labeled task form described in Checkpoint 2.
If it does not work
If the wrong project opens, close the browser tab, open index.html from level-2-task-board, and confirm the loaded app.js path before continuing.

At the end of each later checkpoint:

  1. Run the checkpoint’s browser tests.
  2. Inspect the first Console error and repair it before staging files.
  3. Run git diff and confirm that the diff belongs to the current behavior.
  4. Stage only the files used by the checkpoint.
  5. Commit with a message that names the working result.
  6. Push and confirm the commit on GitHub.
  7. Run git status and leave the working tree clean.

Useful commit outcomes include:

Example checkpoint commit messages
Add accessible task form validation
Render task records from application state
Add task completion state changes
Document and verify task board behavior

Checkpoint 2: Build the form and validation path

Section titled “Checkpoint 2: Build the form and validation path”

Add a task-entry section to index.html. The form needs these persistent labels and controls:

  • Task title text input
  • Category select with the categories from your chosen brief
  • Add task submit button

Add visible title instructions, a dedicated error output, and a status output. Use stable IDs so the HTML and JavaScript agree. Add role="status" to the status output. Connect the title field to its instructions and error with aria-describedby.

In app.js, select the form, title field, category select, error output, and status output. Stop with an explicit error if a required element is absent. Register one submit listener on the form.

Inside the submit handler:

  1. Prevent the form’s default navigation.
  2. Trim the current title value.
  3. If the result is empty, set aria-invalid="true", show Enter a task title., move focus to the title field, and return.
  4. If the result is valid, remove aria-invalid, clear the error, and temporarily log the title and category.

The temporary success log makes the data visible before application state exists. Remove it during Checkpoint 3.

Run these paths in order:

Input and action Visible result State or focus result
Empty title, submit with Enter Enter a task title. Focus is in the title field; no reload
Spaces only, activate the button Same error aria-invalid="true"; no reload
Valid title and category No error Console shows the trimmed title and category once

Use the Elements panel to confirm that the input has aria-invalid="true" only during the invalid state.

Assistance 1 — Identify the form's DOM contract

Write down the ID for the form, title field, category select, error output, and status output. Compare each ID with its querySelector call before you inspect handler logic.

Assistance 2 — Trace a form that reloads or accepts spaces

If the page reloads, confirm that the listener is registered on the form’s submit event and that event.preventDefault() is the first handler action. If spaces pass validation, confirm that the code tests the result of taskTitleInput.value.trim().

Assistance 3 — Build the validation handler in a stable order

Use these subgoals: cancel navigation; read and trim the title; handle the empty path and return; clear the invalid state; read the category; expose the valid data. Do not add state or rendering until all three table rows pass.

Checkpoint: The form exposes valid task data

What now works
The form prevents navigation, rejects empty input with a connected error and focused field, and exposes valid trimmed input once.
Files changed
index.html, app.js, styles.css
What remains
Store valid submissions as task objects and render the complete list and summary from state.
Next action
Open app.js and declare one state object before the submit handler.
If it does not work
If submit behavior differs between Enter and the button, confirm that the listener uses form submit rather than button click.

Checkpoint 3: Store and render task records

Section titled “Checkpoint 3: Store and render task records”

Create one state object in app.js with an empty tasks array. Add a stable way to create a unique numeric ID. The ID only needs to remain unique during the current page load.

Add a task-list section to index.html with:

  • a heading;
  • a summary output;
  • one ul for rendered tasks.

Write one renderTasks function. On every call, the function must:

  1. Remove the previous rendered list content.
  2. Derive the completed count from state.tasks.
  3. Update the visible summary from the completed and total counts.
  4. Show one list item with No tasks yet. when the array is empty.
  5. Otherwise create one list item for each task.
  6. Create each task title and category as text with textContent.
  7. Create one native checkbox for each task and give it a programmatic label.

In the valid submit path, create the task object, add it to state.tasks, call renderTasks, update the status output, reset the form, and return focus to the title field. Remove the temporary success log.

Call renderTasks() once after setup so the initial empty state and 0 of 0 tasks complete summary appear before the first interaction.

  1. Reload the page. Confirm the empty state and zero summary appear.
  2. Add a task whose title contains punctuation, such as Review <header> links & labels.
  3. Confirm the exact text appears as text. It must not create an HTML element.
  4. Add a second task in another category.
  5. Confirm both records remain visible and the summary reports 0 of 2 tasks complete.
  6. Confirm focus returns to the title field after each valid submission.
Assistance 2 — Find why state and the list disagree

Place one temporary console.log(state.tasks) immediately before renderTasks(). If the new object is absent, inspect the submit update. If the object is present but not visible, inspect the render loop and DOM creation. Remove the log after the check.

Assistance 3 — Separate the state update from rendering

The submit handler owns this order: validate → create object → update state → render → report status → reset form → focus title. The render function reads state and builds the current interface. It does not register the form listener or add another task to state.

Checkpoint: Task records render from state

What now works
Valid submissions create task objects, the complete state renders as safe text, the empty state disappears, and the summary uses current state.
Files changed
index.html, app.js, styles.css
What remains
Make each checkbox update the matching task, preserve focus through rendering, and complete the final test pass.
Next action
Inside the task render loop, connect each checkbox change to its task object's completed property.
If it does not work
If one submit creates several records, confirm that the form listener is registered once outside renderTasks.

Checkpoint 4: Update completion state and preserve focus

Section titled “Checkpoint 4: Update completion state and preserve focus”

Inside the render loop, register one change listener on each new checkbox. The handler must:

  1. Update the matching task object’s completed property from the checkbox state.
  2. Call renderTasks with the changed task’s ID.
  3. Update the status output with the task title and its new state.

Let renderTasks accept an optional task ID. After it rebuilds the list, use that ID to find and focus the matching new checkbox. This restores the user’s position after the old checkbox element is replaced.

Add a basic CSS baseline. It must provide:

  • a readable line length and spacing scale;
  • visible default, hover, active, and focus-visible control states;
  • labels that do not depend on placeholders;
  • task titles that wrap instead of widening the page;
  • a complete single-column layout at 320 CSS pixels;
  • a visible completed state that does not depend on color alone.

Do not add project motion yet. A state change must be fully understandable in a browser that displays no animation.

  1. Add two tasks.
  2. Use Tab and Space to complete the second task.
  3. Confirm the second task remains focused after rendering.
  4. Confirm its checkbox is checked, its text treatment identifies completion without color alone, the status names the changed task, and the summary reports 1 of 2 tasks complete.
  5. Uncheck the same task and confirm every result returns to the open state.
  6. Test the complete application at 320 CSS pixels and 200% browser zoom.
  7. Reload and confirm the documented initial empty state returns.
  8. Confirm the Console has no errors.
Assistance 1 — Identify which state should own completion

Inspect the task object associated with the changed checkbox. The checkbox is an interface control; task.completed is the stored value that the next render reads.

Assistance 2 — Repair focus loss after a render

Confirm that every checkbox receives an ID derived from its task ID. Pass the changed task ID into renderTasks, rebuild the list, select the new checkbox with that ID, and call focus() after the new element exists.

Assistance 3 — Verify one completion cycle before styling it

Trace one task through checkbox change → task-object update → complete render → summary derivation → focus restoration → status output. Keep motion and advanced layout out of this checkpoint until that full cycle passes.

Checkpoint: The interactive task board is complete

What now works
A keyboard user can add, validate, read, complete, and reopen fictional tasks while state, summary, status, and focus remain synchronized.
Files changed
index.html, styles.css, app.js, README.md
What remains
Run the complete self-check, record the verified commit, and preserve this repository for the later responsive and animation stage.
Next action
Run every self-check item from a fresh reload and record the exact verified commit in README.md.
If it does not work
If a later check fails, return to the first failing path, inspect state before and after its event, and repair that path before rerunning later checks.

In README.md, record:

  • product name, chosen brief, user, and purpose;
  • the four files and what each file owns;
  • the event → state update → render model;
  • how to open and test the application;
  • the required manual test paths and current result;
  • known limits, including the deliberate lack of persistence;
  • the final verified commit ID;
  • the next stage: responsive components and accessible motion.

Commit and push the final baseline:

Record the verified Stage 1 application
git status
git diff
git add index.html styles.css app.js README.md
git diff --staged
git commit -m "Complete interactive task board"
git push
git status

Do not start Stage 2 in the same commit. The Stage 1 commit is the recovery point for later layout and motion work.

Self-check

Complete these checks against the required result.

  1. A fresh reload shows the documented initial empty state and the correct zero summary without a Console error.
  2. Submitting an empty title and a spaces-only title shows one specific error, changes no task state, and focuses the title field.
  3. A valid title and category create exactly one task object and one rendered list item without a page reload.
  4. A title that contains HTML-looking characters renders as literal text rather than markup.
  5. Two or more tasks remain visible after later submissions because every render uses the complete state array.
  6. Changing a checkbox updates the matching task, the visible completed treatment, the status message, and the derived summary.
  7. The changed checkbox receives focus again after the completion render.
  8. Every required action works with Enter, Tab, and Space where those keys apply, and the focus indicator remains visible.
  9. The interface has no horizontal page scrolling at 320 CSS pixels or 200% browser zoom.
  10. The application has no unresolved Console error, temporary log, unused variable, or commented-out implementation.
  11. README.md states the product, architecture, tests, limits, verified commit, and Stage 2 continuation point.
  12. The verified commit exists in the private GitHub repository and git status reports a clean working tree.

The review uses observable evidence from the required path:

Area Evidence
Event handling Form submit and checkbox change work through registered event listeners without unwanted navigation
State and rendering One state object owns task records; every interface result derives from the current state
Accessibility Labels, validation, keyboard operation, status messages, focus restoration, wrapping, and zoom behavior pass
JavaScript quality DOM selection, validation, state updates, rendering, naming, and safe text insertion have clear roles
Verification The documented test paths pass against one identified commit with a clean Console
Development practice The repository contains focused checkpoints, a useful README, a private remote, and a clean final state

Visual taste is not assessed. The interface must meet the stated usability and accessibility criteria, but the palette, typography, and exact composition remain design choices.

The checkpoint sections provide support for their current behavior. The two options below cover the complete application structure. Compare IDs and responsibilities before copying code into a project with different names.

Assistance 4 — Review a partial application structure

Use this structure when a blank renderer is the active barrier:

Partial app.js structure
"use strict";
// Select the stable interface elements.
const taskForm = document.querySelector("#task-form");
const taskTitleInput = document.querySelector("#task-title");
const taskCategorySelect = document.querySelector("#task-category");
const taskError = document.querySelector("#task-error");
const taskSummary = document.querySelector("#task-summary");
const taskList = document.querySelector("#task-list");
const appStatus = document.querySelector("#app-status");
// Guard the DOM contract.
if (
!taskForm ||
!taskTitleInput ||
!taskCategorySelect ||
!taskError ||
!taskSummary ||
!taskList ||
!appStatus
) {
throw new Error("The task board markup is incomplete.");
}
// Store application data.
const state = {
tasks: [],
};
let nextTaskId = 1;
function renderTasks(focusTaskId) {
// Clear the old list.
// Derive and render the summary.
// Render the empty state or every task.
// Register each checkbox change handler.
// Restore focus when focusTaskId identifies a task.
}
function handleTaskSubmit(event) {
event.preventDefault();
// Read and validate the title.
// Create and store one task object.
// Render, report status, reset, and focus.
}
taskForm.addEventListener("submit", handleTaskSubmit);
renderTasks();

Complete one comment group at a time. Run the related checkpoint test before you continue.

Assistance 5 — Review one complete reference implementationExample solution

This is one valid reference implementation for Brief A. Your names, categories, content, and visual design can differ. The required behavior and accessibility result must remain equivalent.

Reference index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Project delivery board</title>
<link rel="stylesheet" href="styles.css" />
<script src="app.js" defer></script>
</head>
<body>
<header class="site-header">
<p class="eyebrow">Studio practice</p>
<h1>Project delivery board</h1>
<p>Add fictional project tasks and track their completion state.</p>
</header>
<main>
<section aria-labelledby="add-task-heading">
<h2 id="add-task-heading">Add a task</h2>
<form id="task-form" novalidate>
<div class="field">
<label for="task-title">Task title</label>
<input
id="task-title"
name="taskTitle"
type="text"
maxlength="80"
aria-describedby="task-title-help task-error"
/>
<p id="task-title-help">Use 1–80 characters.</p>
<p id="task-error" class="error"></p>
</div>
<div class="field">
<label for="task-category">Category</label>
<select id="task-category" name="taskCategory">
<option value="Content">Content</option>
<option value="Design">Design</option>
<option value="Development">Development</option>
<option value="Review">Review</option>
</select>
</div>
<button type="submit">Add task</button>
</form>
</section>
<section aria-labelledby="task-list-heading">
<div class="section-heading">
<h2 id="task-list-heading">Current tasks</h2>
<p id="task-summary">0 of 0 tasks complete</p>
</div>
<ul id="task-list" class="task-list"></ul>
</section>
<p id="app-status" class="status" role="status">
Add a task to begin.
</p>
</main>
</body>
</html>
Reference styles.css
:root {
--surface: #f5f7f4;
--surface-raised: #fcfdfb;
--text: #18231f;
--muted: #52645d;
--border: #a7b4af;
--accent: #1f6f5f;
--error: #b42318;
--space-1: 0.5rem;
--space-2: 0.75rem;
--space-3: 1rem;
--space-4: 1.5rem;
--space-5: 2rem;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background: var(--surface);
color: var(--text);
font-family: system-ui, sans-serif;
line-height: 1.5;
}
.site-header,
main {
width: min(100% - 2rem, 48rem);
margin-inline: auto;
}
.site-header {
padding-block: var(--space-5) var(--space-3);
}
main {
display: grid;
gap: var(--space-5);
padding-block-end: var(--space-5);
}
h1,
h2,
p {
overflow-wrap: anywhere;
}
.eyebrow {
margin: 0;
color: var(--muted);
font-weight: 700;
text-transform: uppercase;
}
form,
.task-list {
display: grid;
gap: var(--space-3);
}
.field {
display: grid;
gap: var(--space-1);
}
label {
font-weight: 700;
}
input,
select,
button {
min-height: 2.75rem;
border: 1px solid var(--border);
border-radius: 0.35rem;
padding: var(--space-2);
font: inherit;
}
button {
border-color: var(--accent);
background: var(--accent);
color: white;
font-weight: 700;
cursor: pointer;
}
button:hover {
filter: brightness(1.1);
}
button:active {
filter: brightness(0.9);
}
:focus-visible {
outline: 0.2rem solid currentColor;
outline-offset: 0.2rem;
}
[aria-invalid="true"] {
border-color: var(--error);
border-width: 0.15rem;
}
.error {
min-height: 1.5em;
margin: 0;
color: var(--error);
font-weight: 700;
}
.section-heading {
display: flex;
align-items: baseline;
flex-wrap: wrap;
justify-content: space-between;
gap: var(--space-2);
}
.section-heading > * {
margin-block: 0;
}
.task-list {
margin: var(--space-3) 0 0;
padding: 0;
list-style: none;
}
.task-card {
border: 1px solid var(--border);
border-radius: 0.5rem;
background: var(--surface-raised);
padding: var(--space-3);
}
.task-control {
display: flex;
align-items: flex-start;
gap: var(--space-2);
}
.task-control input {
flex: 0 0 auto;
}
.task-copy {
display: grid;
min-width: 0;
gap: var(--space-1);
}
.task-title,
.task-category {
overflow-wrap: anywhere;
}
.task-card[data-completed="true"] .task-title {
text-decoration: line-through;
}
.task-category,
.status {
color: var(--muted);
}
.status {
min-height: 1.5em;
margin: 0;
}
Reference app.js
"use strict";
// Select the stable interface elements.
const taskForm = document.querySelector("#task-form");
const taskTitleInput = document.querySelector("#task-title");
const taskCategorySelect = document.querySelector("#task-category");
const taskError = document.querySelector("#task-error");
const taskSummary = document.querySelector("#task-summary");
const taskList = document.querySelector("#task-list");
const appStatus = document.querySelector("#app-status");
if (
!taskForm ||
!taskTitleInput ||
!taskCategorySelect ||
!taskError ||
!taskSummary ||
!taskList ||
!appStatus
) {
throw new Error("The task board markup is incomplete.");
}
// Keep task records in one application state object.
const state = {
tasks: [],
};
let nextTaskId = 1;
function renderTasks(focusTaskId) {
taskList.replaceChildren();
const completedCount = state.tasks.filter((task) => task.completed).length;
taskSummary.textContent = `${completedCount} of ${state.tasks.length} tasks complete`;
if (state.tasks.length === 0) {
const emptyItem = document.createElement("li");
emptyItem.textContent = "No tasks yet.";
taskList.append(emptyItem);
return;
}
state.tasks.forEach((task) => {
const item = document.createElement("li");
item.className = "task-card";
item.dataset.completed = String(task.completed);
const label = document.createElement("label");
label.className = "task-control";
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.id = `task-${task.id}`;
checkbox.checked = task.completed;
const copy = document.createElement("span");
copy.className = "task-copy";
const title = document.createElement("span");
title.className = "task-title";
title.textContent = task.title;
const category = document.createElement("span");
category.className = "task-category";
category.textContent = task.category;
copy.append(title, category);
label.append(checkbox, copy);
item.append(label);
taskList.append(item);
checkbox.addEventListener("change", () => {
task.completed = checkbox.checked;
renderTasks(task.id);
const stateLabel = task.completed ? "complete" : "open";
appStatus.textContent = `${task.title} is now ${stateLabel}.`;
});
});
if (focusTaskId !== undefined) {
const checkboxToFocus = document.querySelector(`#task-${focusTaskId}`);
if (checkboxToFocus) {
checkboxToFocus.focus();
}
}
}
function handleTaskSubmit(event) {
event.preventDefault();
const taskTitle = taskTitleInput.value.trim();
if (taskTitle === "") {
taskTitleInput.setAttribute("aria-invalid", "true");
taskError.textContent = "Enter a task title.";
taskTitleInput.focus();
return;
}
taskTitleInput.removeAttribute("aria-invalid");
taskError.textContent = "";
const task = {
id: nextTaskId,
title: taskTitle,
category: taskCategorySelect.value,
completed: false,
};
nextTaskId += 1;
state.tasks.push(task);
renderTasks();
appStatus.textContent = `${task.title} was added.`;
taskForm.reset();
taskTitleInput.focus();
}
taskForm.addEventListener("submit", handleTaskSubmit);
renderTasks();

After you use the reference, close it and explain your own event-state-render cycle from app.js. Verify all self-check paths against your implementation.

Stop only at a passing checkpoint when possible. Commit and push that state. Add this note to README.md or a private project note before a longer interruption:

Resume note
## Resume here
- What works:
- First failing or unfinished test:
- Next action:
- File and function to open:
- Last passing commit:
- Required browser or server state:

When the required assignment is complete, preserve the repository and final commit. The later responsive and animation assignment will continue from this exact application instead of starting another product.