# Web Developer Mastery Lab — 50 workshops

DiscoveryVIP · October 9, 2026

Body HTML, CSS and JavaScript run inside an isolated preview. External network access and form submissions are blocked. Automated checks are selected teaching checks, not a complete quality or accessibility audit.

## 1. Create a meaningful heading

HTML describes meaning and structure. A heading announces a section rather than merely making text large. Use a page-level h1 to describe the main topic, and let CSS decide its visual size. The preview supplies the document shell; you edit the body fragment.

Challenge: Add one h1 containing My first website and a paragraph introducing it.

Hint: Use <h1> for the topic and <p> for the introduction.

```html
<h1>My first website</h1><p>I am learning to build useful pages.</p>
```
```css
body { font: 16px/1.6 system-ui; margin: 24px; color: #233b46; }
button, input { font: inherit; padding: 8px; }

```

Manual review: Use the preview with the keyboard, then try a narrow viewport. Explain what you would check manually before publishing.

## 2. Structure a page with landmarks

header, nav, main and footer describe the roles of major page regions. A main element identifies the primary content. Meaningful structure helps navigation and makes a page easier to maintain. Avoid using multiple visible main elements for unrelated layout boxes.

Challenge: Build header, nav, main and footer. Give main an h1.

Hint: Use semantic elements rather than four anonymous divs.

```html
<header>Studio</header><nav aria-label="Main">Home</nav><main><h1>Our studio</h1></main><footer>Contact us</footer>
```
```css
body { font: 16px/1.6 system-ui; margin: 24px; color: #233b46; }
button, input { font: inherit; padding: 8px; }

```

Manual review: Use the preview with the keyboard, then try a narrow viewport. Explain what you would check manually before publishing.

## 3. Build a heading hierarchy

Headings give the page an outline. Choose h2 for sections under the page h1, and h3 for a subsection under an h2. Do not choose a heading level just to get a smaller font. A good outline remains useful even when styling is removed.

Challenge: Create an h1 and two sections, each with an h2.

Hint: Each section needs its own h2.

```html
<h1>Learning path</h1><section><h2>Plan</h2><p>Choose a goal.</p></section><section><h2>Build</h2><p>Try an idea.</p></section>
```
```css
body { font: 16px/1.6 system-ui; margin: 24px; color: #233b46; }
button, input { font: inherit; padding: 8px; }

```

Manual review: Use the preview with the keyboard, then try a narrow viewport. Explain what you would check manually before publishing.

## 4. Write a useful link

An anchor with href is for navigation. Its visible text should describe the destination without relying on nearby prose. Fragment links point to an element ID in the same document. Buttons are better for actions that change the current interface.

Challenge: Add a link to #projects and a section with id projects.

Hint: The href fragment and section id must match.

```html
<a href="#projects">Explore my projects</a><section id="projects"><h2>Projects</h2><p>A portfolio starts here.</p></section>
```
```css
body { font: 16px/1.6 system-ui; margin: 24px; color: #233b46; }
button, input { font: inherit; padding: 8px; }

```

Manual review: Use the preview with the keyboard, then try a narrow viewport. Explain what you would check manually before publishing.

## 5. Organize lists

Use ul when order is not meaningful and ol when sequence matters. Each item belongs in an li. Lists convey grouping to assistive technology as well as providing visual structure. Avoid typing bullet characters into unrelated paragraphs.

Challenge: Create an ordered list with three steps.

Hint: Place three li elements inside ol.

```html
<h1>Launch checklist</h1><ol><li>Plan</li><li>Build</li><li>Test</li></ol>
```
```css
body { font: 16px/1.6 system-ui; margin: 24px; color: #233b46; }
button, input { font: inherit; padding: 8px; }

```

Manual review: Use the preview with the keyboard, then try a narrow viewport. Explain what you would check manually before publishing.

## 6. Give images a text alternative

An image needs an alt decision based on purpose. Informative images need a concise equivalent; decorative images use an empty alt. The same image can have different roles in different contexts. This exercise uses a local data image so the preview needs no network.

Challenge: Add an image with alt="A blue square" using the provided data image.

Hint: An alt attribute belongs on img, not on a surrounding paragraph.

```html
<img src="data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%22120%22 height=%22120%22%3E%3Crect width=%22120%22 height=%22120%22 fill=%22steelblue%22/%3E%3C/svg%3E" alt="A blue square">
```
```css
body { font: 16px/1.6 system-ui; margin: 24px; color: #233b46; }
button, input { font: inherit; padding: 8px; }

```

Manual review: Use the preview with the keyboard, then try a narrow viewport. Explain what you would check manually before publishing.

## 7. Caption a figure

figure groups self-contained media with its optional figcaption. A caption can provide context beyond the image alternative, but it does not automatically replace alt text. Keep the figure understandable as a unit.

Challenge: Make a figure with an image and a figcaption.

Hint: Group the image and figcaption inside figure.

```html
<figure><img src="data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2280%22 height=%2280%22%3E%3Ccircle cx=%2240%22 cy=%2240%22 r=%2235%22 fill=%22teal%22/%3E%3C/svg%3E" alt="Teal circle"><figcaption>Shape study, number one.</figcaption></figure>
```
```css
body { font: 16px/1.6 system-ui; margin: 24px; color: #233b46; }
button, input { font: inherit; padding: 8px; }

```

Manual review: Use the preview with the keyboard, then try a narrow viewport. Explain what you would check manually before publishing.

## 8. Choose a native button

A button provides keyboard activation and focus behavior. A clickable div does not automatically provide these features. Specify type="button" for a non-submit action, especially if it might later be placed in a form.

Challenge: Add a button labeled Save draft with type button.

Hint: Use a real button element and set its type.

```html
<button type="button">Save draft</button>
```
```css
body { font: 16px/1.6 system-ui; margin: 24px; color: #233b46; }
button, input { font: inherit; padding: 8px; }

```

Manual review: Use the preview with the keyboard, then try a narrow viewport. Explain what you would check manually before publishing.

## 9. Display tabular data

A table is appropriate for relationships between rows and columns, not for page layout. Header cells identify the meaning of data. A caption names the table. scope helps explain whether a header applies to a row or a column.

Challenge: Build a table with a caption, two column headers and one data row.

Hint: Use th for headings and td for data.

```html
<table><caption>Project status</caption><thead><tr><th scope="col">Project</th><th scope="col">Status</th></tr></thead><tbody><tr><td>Portfolio</td><td>Draft</td></tr></tbody></table>
```
```css
body { font: 16px/1.6 system-ui; margin: 24px; color: #233b46; }
button, input { font: inherit; padding: 8px; }
th,td { padding: 8px; border: 1px solid #aaa; }
```

Manual review: Use the preview with the keyboard, then try a narrow viewport. Explain what you would check manually before publishing.

## 10. Build a disclosure without JavaScript

details and summary provide a built-in disclosure control. Users can toggle it with a mouse or keyboard. Native components reduce the amount of custom behavior you need to reproduce and test. Put a clear summary first.

Challenge: Create a details element with a summary and an explanatory paragraph.

Hint: Use details to wrap summary and the expanded content.

```html
<details><summary>How does this course work?</summary><p>Build, preview and improve one feature at a time.</p></details>
```
```css
body { font: 16px/1.6 system-ui; margin: 24px; color: #233b46; }
button, input { font: inherit; padding: 8px; }

```

Manual review: Use the preview with the keyboard, then try a narrow viewport. Explain what you would check manually before publishing.

## 11. Associate a label and input

A visible label explains a form field and gives it an accessible name when properly associated. Use for matching the input id, or nest the input inside its label. Placeholder text disappears and is not a reliable substitute.

Challenge: Add a visible label for an input with id name.

Hint: Match label for="name" with input id="name".

```html
<label for="name">Your name</label><input id="name" name="name" autocomplete="name">
```
```css
body { font: 16px/1.6 system-ui; margin: 24px; color: #233b46; }
button, input { font: inherit; padding: 8px; }

```

Manual review: Use the preview with the keyboard, then try a narrow viewport. Explain what you would check manually before publishing.

## 12. Use built-in validation

HTML input types and attributes provide basic validation before JavaScript. type=email checks an address-like syntax, not whether the mailbox exists. required rejects an empty value. Server-side validation remains necessary for a real submission.

Challenge: Create a required email input with a label.

Hint: Use type="email" and the boolean required attribute.

```html
<label for="email">Email</label><input id="email" name="email" type="email" required>
```
```css
body { font: 16px/1.6 system-ui; margin: 24px; color: #233b46; }
button, input { font: inherit; padding: 8px; }

```

Manual review: Use the preview with the keyboard, then try a narrow viewport. Explain what you would check manually before publishing.

## 13. Group related form controls

fieldset groups related controls and legend names that group. Radios with the same name form a choice set. Give every radio its own label so both the group and each option make sense.

Challenge: Create a fieldset with a legend and two radios named plan.

Hint: Wrap each radio inside its label.

```html
<fieldset><legend>Choose a plan</legend><label><input type="radio" name="plan" value="basic"> Basic</label><label><input type="radio" name="plan" value="plus"> Plus</label></fieldset>
```
```css
body { font: 16px/1.6 system-ui; margin: 24px; color: #233b46; }
button, input { font: inherit; padding: 8px; }

```

Manual review: Use the preview with the keyboard, then try a narrow viewport. Explain what you would check manually before publishing.

## 14. Offer a select menu

select works well for a compact set of predefined choices. Each option has a value for program logic and a label for people. A visible associated label explains what the menu controls. Avoid using a select as an unlabeled decoration.

Challenge: Create a labeled select with id topic and three options.

Hint: Use a select, not three unrelated buttons.

```html
<label for="topic">Choose a topic</label><select id="topic"><option value="html">HTML</option><option value="css">CSS</option><option value="js">JavaScript</option></select>
```
```css
body { font: 16px/1.6 system-ui; margin: 24px; color: #233b46; }
button, input { font: inherit; padding: 8px; }

```

Manual review: Use the preview with the keyboard, then try a narrow viewport. Explain what you would check manually before publishing.

## 15. Connect help text to a field

aria-describedby associates additional instructions with a control. It supplements rather than replaces a label. Keep the guidance concise and available visually too. For errors, explain what needs changing rather than relying only on color.

Challenge: Make an input with id username, label, and aria-describedby="help" pointing to guidance.

Hint: IDs connect a control to its separate guidance.

```html
<label for="username">Username</label><input id="username" aria-describedby="help"><p id="help">Use a public nickname, not your email.</p>
```
```css
body { font: 16px/1.6 system-ui; margin: 24px; color: #233b46; }
button, input { font: inherit; padding: 8px; }

```

Manual review: Use the preview with the keyboard, then try a narrow viewport. Explain what you would check manually before publishing.

## 16. Target a CSS class

A class selector lets several elements share presentation. HTML provides the class name; CSS prefixes it with a dot. Keep selectors focused on reusable roles rather than styling every element of a type globally.

Challenge: Create a .notice paragraph and give it a background of rgb(230, 245, 255).

Hint: Use .notice { background-color: ...; }.

```html
<p class="notice">Your changes are saved.</p>
```
```css
body { font: 16px/1.6 system-ui; margin: 24px; color: #233b46; }
button, input { font: inherit; padding: 8px; }
.notice { background-color: rgb(230,245,255); padding:16px; }
```

Manual review: Use the preview with the keyboard, then try a narrow viewport. Explain what you would check manually before publishing.

## 17. Control the box model

By default, an element’s declared width excludes padding and borders. border-box includes them, often making layout calculations easier. Margin remains outside either sizing model. Inspect computed sizes instead of guessing from appearance.

Challenge: Create .box with width 200px, padding 20px and border-box sizing.

Hint: Set box-sizing: border-box on the box.

```html
<div class="box">A predictable box</div>
```
```css
body { font: 16px/1.6 system-ui; margin: 24px; color: #233b46; }
button, input { font: inherit; padding: 8px; }
.box { width:200px; padding:20px; box-sizing:border-box; border:2px solid teal; }
```

Manual review: Use the preview with the keyboard, then try a narrow viewport. Explain what you would check manually before publishing.

## 18. Make text comfortable to read

Readable typography depends on size, line spacing, contrast and line length. A unitless line-height scales with font size. Avoid shrinking text just to squeeze more content into a fixed box. Test with zoom and real content lengths.

Challenge: Style .copy with font-size 20px and line-height 1.6.

Hint: A 20px font with 1.6 line-height produces a 32px line box.

```html
<p class="copy">Good typography makes a useful idea easier to understand.</p>
```
```css
body { font: 16px/1.6 system-ui; margin: 24px; color: #233b46; }
button, input { font: inherit; padding: 8px; }
.copy { font-size:20px; line-height:1.6; max-width:60ch; }
```

Manual review: Use the preview with the keyboard, then try a narrow viewport. Explain what you would check manually before publishing.

## 19. Use custom properties

CSS custom properties hold reusable values and participate in the cascade. var reads the value where a property is used. Centralizing a color or spacing value helps a design stay consistent. A variable name alone has no visual effect until consumed.

Challenge: Define --accent as teal on :root and use it as .tag text color.

Hint: Define the token on :root and use var(--accent).

```html
<span class="tag">New lesson</span>
```
```css
body { font: 16px/1.6 system-ui; margin: 24px; color: #233b46; }
button, input { font: inherit; padding: 8px; }
:root { --accent:teal; } .tag { color:var(--accent); }
```

Manual review: Use the preview with the keyboard, then try a narrow viewport. Explain what you would check manually before publishing.

## 20. Resolve competing selectors

The cascade decides between declarations using origin, importance, layers, specificity and source order. In this small exercise, two equally specific class rules compete, so the later one wins. Avoid reaching for !important before understanding why a rule loses.

Challenge: Make .badge text blue using a later class rule after a red rule.

Hint: Put the blue declaration after the equally specific red declaration.

```html
<span class="badge">Ready</span>
```
```css
body { font: 16px/1.6 system-ui; margin: 24px; color: #233b46; }
button, input { font: inherit; padding: 8px; }
.badge { color:red; }
.badge { color:blue; }
```

Manual review: Use the preview with the keyboard, then try a narrow viewport. Explain what you would check manually before publishing.

## 21. Lay out a flex row

Flexbox arranges children along a main axis. gap adds consistent space between items. It is useful for navigation, controls and small one-dimensional layouts. The children remain in DOM order unless you deliberately change their visual order.

Challenge: Make .toolbar a flex container with a 12px gap and two buttons.

Hint: Set display:flex and gap on the parent.

```html
<div class="toolbar"><button>Save</button><button>Preview</button></div>
```
```css
body { font: 16px/1.6 system-ui; margin: 24px; color: #233b46; }
button, input { font: inherit; padding: 8px; }
.toolbar { display:flex; gap:12px; }
```

Manual review: Use the preview with the keyboard, then try a narrow viewport. Explain what you would check manually before publishing.

## 22. Align items on two axes

justify-content distributes space along the main axis; align-items controls the cross axis. Their visual direction changes with flex-direction. Use these properties intentionally rather than adding arbitrary margins to children.

Challenge: Center .item inside a 160px-tall flex .stage using both alignment properties.

Hint: Both alignment declarations belong on .stage.

```html
<div class="stage"><div class="item">Centered</div></div>
```
```css
body { font: 16px/1.6 system-ui; margin: 24px; color: #233b46; }
button, input { font: inherit; padding: 8px; }
.stage { display:flex; height:160px; justify-content:center; align-items:center; background:#edf4f6; }
```

Manual review: Use the preview with the keyboard, then try a narrow viewport. Explain what you would check manually before publishing.

## 23. Allow controls to wrap

A row that works on a wide screen can overflow on a phone. flex-wrap allows items to move onto another line. Wrapping is often better than making labels too small to read. Test the longest likely label, not just a short sample.

Challenge: Build .chips as a wrapping flex container with at least three items.

Hint: Use flex-wrap: wrap, then narrow the preview.

```html
<div class="chips"><span>HTML foundations</span><span>Responsive CSS</span><span>Browser interactions</span></div>
```
```css
body { font: 16px/1.6 system-ui; margin: 24px; color: #233b46; }
button, input { font: inherit; padding: 8px; }
.chips { display:flex; flex-wrap:wrap; gap:12px; } .chips span { padding:12px; background:#e7f0ed; }
```

Manual review: Use the preview with the keyboard, then try a narrow viewport. Explain what you would check manually before publishing.

## 24. Build a two-column grid

Grid is useful when rows and columns need to work together. Fractional units share available space. minmax(0,1fr) allows content to shrink inside a track instead of forcing unexpected overflow. Grid does not replace sensible source order.

Challenge: Create .grid with two equal columns and two cards.

Hint: Use repeat(2, minmax(0, 1fr)).

```html
<div class="grid"><article>Plan</article><article>Build</article></div>
```
```css
body { font: 16px/1.6 system-ui; margin: 24px; color: #233b46; }
button, input { font: inherit; padding: 8px; }
.grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:16px; } .grid article { background:#edf4f6; padding:20px; }
```

Manual review: Use the preview with the keyboard, then try a narrow viewport. Explain what you would check manually before publishing.

## 25. Make a centered content container

A content container can fill small screens while limiting line length on large screens. max-width supplies the limit and auto inline margins center the box when space remains. Fixed widths alone can cause horizontal scrolling.

Challenge: Create .container with max-width 700px and automatic left/right margins.

Hint: Use max-width, not a forced 700px width.

```html
<main class="container"><h1>A focused reading space</h1><p>Good content has room to breathe.</p></main>
```
```css
body { font: 16px/1.6 system-ui; margin: 24px; color: #233b46; }
button, input { font: inherit; padding: 8px; }
.container { max-width:700px; margin-inline:auto; }
```

Manual review: Use the preview with the keyboard, then try a narrow viewport. Explain what you would check manually before publishing.

## 26. Switch layout at a breakpoint

A media query applies CSS only when a condition matches. Start with a usable narrow layout, then add columns when there is room. Breakpoints should follow content needs rather than a specific phone model. Inspect both sides of the boundary.

Challenge: Make .responsive one column below 600px and two columns at 600px or wider.

Hint: Use a min-width:600px query and test both preview sizes.

```html
<div class="responsive"><article>Learn</article><article>Practice</article></div>
```
```css
body { font: 16px/1.6 system-ui; margin: 24px; color: #233b46; }
button, input { font: inherit; padding: 8px; }
.responsive { display:grid; grid-template-columns:1fr; gap:16px; } @media(min-width:600px) { .responsive { grid-template-columns:1fr 1fr; } }
```

Manual review: Use the preview with the keyboard, then try a narrow viewport. Explain what you would check manually before publishing.

## 27. Scale an image without overflow

An image can retain its natural aspect ratio while shrinking to fit its container. max-width:100% prevents it from exceeding available width and height:auto preserves the ratio. Large source dimensions should not dictate page width.

Challenge: Add .photo with max-width 100% and height auto.

Hint: Do not set a fixed height that distorts the image.

```html
<img class="photo" alt="Wide blue banner" src="data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%22800%22 height=%22200%22%3E%3Crect width=%22800%22 height=%22200%22 fill=%22steelblue%22/%3E%3C/svg%3E">
```
```css
body { font: 16px/1.6 system-ui; margin: 24px; color: #233b46; }
button, input { font: inherit; padding: 8px; }
.photo { max-width:100%; height:auto; }
```

Manual review: Use the preview with the keyboard, then try a narrow viewport. Explain what you would check manually before publishing.

## 28. Contain a long word

A single long URL or identifier can force a narrow card wider than its container. overflow-wrap:anywhere allows it to break when necessary. This solves a content problem without hiding the text. Avoid using overflow:hidden just to disguise clipping.

Challenge: Style .url with overflow-wrap anywhere.

Hint: Allow wrapping rather than clipping.

```html
<p class="url">averylongexampleidentifierwithoutspaceswhichneedstowrapinsideanarrowlayout</p>
```
```css
body { font: 16px/1.6 system-ui; margin: 24px; color: #233b46; }
button, input { font: inherit; padding: 8px; }
.url { max-width:220px; overflow-wrap:anywhere; }
```

Manual review: Use the preview with the keyboard, then try a narrow viewport. Explain what you would check manually before publishing.

## 29. Make a scrollable table region

Some tabular relationships need more horizontal space than a small screen has. A dedicated scroll region can preserve the table while containing overflow. Give the region a name and keyboard focus so users can reach it.

Challenge: Wrap a wide table in .table-wrap with overflow-x auto and tabindex 0.

Hint: Apply overflow to the wrapper, not the page.

```html
<div class="table-wrap" tabindex="0" role="region" aria-label="Project data"><table><tr><th>Project</th><th>Status</th></tr><tr><td>Portfolio</td><td>In progress</td></tr></table></div>
```
```css
body { font: 16px/1.6 system-ui; margin: 24px; color: #233b46; }
button, input { font: inherit; padding: 8px; }
.table-wrap { overflow-x:auto; } table { min-width:700px; }
```

Manual review: Use the preview with the keyboard, then try a narrow viewport. Explain what you would check manually before publishing.

## 30. Project: responsive card collection

Combine a heading, semantic cards and a layout that adjusts to space. repeat(auto-fit,minmax(...)) can create columns without hardcoding their count. Make the minimum track size compatible with narrow screens.

Challenge: Build .cards with three article cards, each containing an h2; use CSS Grid.

Hint: Use a minimum track width that never exceeds 100% of available space.

```html
<h1>Learning projects</h1><div class="cards"><article><h2>Portfolio</h2><p>Tell your story.</p></article><article><h2>Product</h2><p>Explain a benefit.</p></article><article><h2>Dashboard</h2><p>Show useful data.</p></article></div>
```
```css
body { font: 16px/1.6 system-ui; margin: 24px; color: #233b46; }
button, input { font: inherit; padding: 8px; }
.cards { display:grid; grid-template-columns:repeat(auto-fit,minmax(min(100%,200px),1fr)); gap:16px; } article { padding:18px; background:#edf4f6; }
```

Manual review: Use the preview with the keyboard, then try a narrow viewport. Explain what you would check manually before publishing.

## 31. Make focus visible

Keyboard users need to know which control is active. A visible outline should contrast with nearby colors and must not be removed without replacement. This exercise checks a focus style, but real accessibility also requires testing the whole focus order.

Challenge: Style button:focus with a 3px solid outline.

Hint: Use :focus for this exercise, then press Tab in the preview.

```html
<button>First action</button><button>Second action</button>
```
```css
body { font: 16px/1.6 system-ui; margin: 24px; color: #233b46; }
button, input { font: inherit; padding: 8px; }
button:focus { outline:3px solid #075b87; outline-offset:3px; }
```

Manual review: Use the preview with the keyboard, then try a narrow viewport. Explain what you would check manually before publishing.

## 32. Add a skip link

A skip link lets keyboard users bypass repeated navigation. Its destination should identify the main content. Make the link visible when focused if you hide it visually at rest. This exercise leaves it visible to make its purpose obvious.

Challenge: Add a Skip to content link to #main and a main destination.

Hint: Link to the main element’s ID.

```html
<a href="#main">Skip to content</a><nav>Home · Projects · About</nav><main id="main" tabindex="-1"><h1>Main content</h1></main>
```
```css
body { font: 16px/1.6 system-ui; margin: 24px; color: #233b46; }
button, input { font: inherit; padding: 8px; }

```

Manual review: Use the preview with the keyboard, then try a narrow viewport. Explain what you would check manually before publishing.

## 33. Provide a status region

A polite live region can announce a meaningful update without moving focus. Set it up before the update happens. Do not put a whole rapidly changing page into a live region; only announce useful information.

Challenge: Create #status with role status and text Ready.

Hint: role="status" provides polite live-region semantics.

```html
<p id="status" role="status">Ready</p>
```
```css
body { font: 16px/1.6 system-ui; margin: 24px; color: #233b46; }
button, input { font: inherit; padding: 8px; }

```

Manual review: Use the preview with the keyboard, then try a narrow viewport. Explain what you would check manually before publishing.

## 34. Use consistent spacing tokens

A small spacing scale helps repeated components feel related. Custom properties make the relationship visible in the stylesheet. Tokens are useful only when their names and values serve a real design need; avoid dozens of nearly identical values.

Challenge: Define --space as 16px and use it for .panel padding.

Hint: Use var(--space) as the padding value.

```html
<section class="panel"><h2>Consistent spacing</h2><p>One token, one clear role.</p></section>
```
```css
body { font: 16px/1.6 system-ui; margin: 24px; color: #233b46; }
button, input { font: inherit; padding: 8px; }
:root { --space:16px; } .panel { padding:var(--space); background:#edf4f6; }
```

Manual review: Use the preview with the keyboard, then try a narrow viewport. Explain what you would check manually before publishing.

## 35. Reserve image space

Supplying image dimensions helps the browser reserve an aspect ratio before loading. This reduces unexpected movement. CSS can still make the image responsive. Dimensions do not replace alternative text or good compression.

Challenge: Add an image with width 120, height 80 and an alt attribute.

Hint: Use numeric width and height attributes on img.

```html
<img width="120" height="80" alt="Sample landscape placeholder" src="data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%22120%22 height=%2280%22%3E%3Crect width=%22120%22 height=%2280%22 fill=%22teal%22/%3E%3C/svg%3E">
```
```css
body { font: 16px/1.6 system-ui; margin: 24px; color: #233b46; }
button, input { font: inherit; padding: 8px; }

```

Manual review: Use the preview with the keyboard, then try a narrow viewport. Explain what you would check manually before publishing.

## 36. Change text with JavaScript

querySelector finds the first matching element. textContent writes text rather than interpreting it as HTML. Use it for user-controlled strings unless you have a specific, carefully controlled reason to create markup. The script runs after the exercise HTML exists.

Challenge: Change #message from Waiting to Hello, browser! using JavaScript.

Hint: Select #message, then assign textContent.

```html
<p id="message">Waiting</p>
```
```css
body { font: 16px/1.6 system-ui; margin: 24px; color: #233b46; }
button, input { font: inherit; padding: 8px; }

```
```js
document.querySelector("#message").textContent = "Hello, browser!";
```

Manual review: Use the preview with the keyboard, then try a narrow viewport. Explain what you would check manually before publishing.

## 37. Respond to a click

addEventListener connects an event to a handler. A handler is a function to run later; passing the result of calling it immediately is a common mistake. Keep the button semantic so keyboard activation works too.

Challenge: Make clicking #save set #status to Saved.

Hint: Pass a function to addEventListener.

```html
<button id="save">Save</button><p id="status" role="status">Waiting</p>
```
```css
body { font: 16px/1.6 system-ui; margin: 24px; color: #233b46; }
button, input { font: inherit; padding: 8px; }

```
```js
document.querySelector("#save").addEventListener("click", () => { document.querySelector("#status").textContent="Saved"; });
```

Manual review: Use the preview with the keyboard, then try a narrow viewport. Explain what you would check manually before publishing.

## 38. Keep state and display in sync

A counter has a state value and a rendered representation. Update the state first, then render the new value. Reading arbitrary text as your only state can become fragile as the UI grows. Start from a known initial value.

Challenge: Make #add increment #count from 0 on every click.

Hint: Keep count in a let binding.

```html
<button id="add">Add one</button><output id="count">0</output>
```
```css
body { font: 16px/1.6 system-ui; margin: 24px; color: #233b46; }
button, input { font: inherit; padding: 8px; }

```
```js
let count=0; document.querySelector("#add").addEventListener("click",()=>{count++;document.querySelector("#count").textContent=count;});
```

Manual review: Use the preview with the keyboard, then try a narrow viewport. Explain what you would check manually before publishing.

## 39. Toggle a CSS class

classList lets JavaScript change a state class without rewriting the entire style attribute. CSS remains responsible for appearance. An action can also update aria-pressed when it represents a toggle button, keeping the state understandable.

Challenge: Make #toggle toggle the active class on #panel.

Hint: Use classList.toggle("active").

```html
<button id="toggle">Toggle panel</button><div id="panel">Panel</div>
```
```css
body { font: 16px/1.6 system-ui; margin: 24px; color: #233b46; }
button, input { font: inherit; padding: 8px; }
.active { background:gold; }
```
```js
document.querySelector("#toggle").addEventListener("click",()=>document.querySelector("#panel").classList.toggle("active"));
```

Manual review: Use the preview with the keyboard, then try a narrow viewport. Explain what you would check manually before publishing.

## 40. Read an input event

The input event fires as a user edits a value. Read the current value from the control, then update the display. Do not use innerHTML to echo arbitrary input. This is a small example of rendering state in response to an event.

Challenge: Make #name input update #greeting to Hello, VALUE.

Hint: Use e.target.value in the input handler.

```html
<label for="name">Name</label><input id="name"><p id="greeting">Hello, </p>
```
```css
body { font: 16px/1.6 system-ui; margin: 24px; color: #233b46; }
button, input { font: inherit; padding: 8px; }

```
```js
document.querySelector("#name").addEventListener("input",e=>{document.querySelector("#greeting").textContent="Hello, "+e.target.value;});
```

Manual review: Use the preview with the keyboard, then try a narrow viewport. Explain what you would check manually before publishing.

## 41. Create a list item safely

createElement makes a DOM node and textContent supplies its text. append adds it to a parent. This avoids interpreting a string as markup. For a large list, consider batching changes rather than repeatedly forcing layout reads between writes.

Challenge: Use JavaScript to append an li with text Practice to #tasks.

Hint: Create the li, set textContent and append it.

```html
<ul id="tasks"></ul>
```
```css
body { font: 16px/1.6 system-ui; margin: 24px; color: #233b46; }
button, input { font: inherit; padding: 8px; }

```
```js
const li=document.createElement("li");li.textContent="Practice";document.querySelector("#tasks").append(li);
```

Manual review: Use the preview with the keyboard, then try a narrow viewport. Explain what you would check manually before publishing.

## 42. Handle a local form submission

A form submit event supports both a submit button and keyboard submission. preventDefault stops navigation when your code handles the action locally. A real service still needs a backend and server validation; this exercise only updates the page.

Challenge: Prevent submission navigation and set #result to Received when #form submits.

Hint: Listen to submit on the form, not only click on the button.

```html
<form id="form"><label for="note">Note</label><input id="note"><button type="submit">Send</button></form><p id="result" role="status"></p>
```
```css
body { font: 16px/1.6 system-ui; margin: 24px; color: #233b46; }
button, input { font: inherit; padding: 8px; }

```
```js
document.querySelector("#form").addEventListener("submit",e=>{e.preventDefault();document.querySelector("#result").textContent="Received";});
```

Manual review: Use the preview with the keyboard, then try a narrow viewport. Explain what you would check manually before publishing.

## 43. Build a disclosure state

A custom disclosure must keep visibility and its button state aligned. hidden removes the content from rendering. aria-expanded communicates whether the controlled content is expanded. For simpler cases, prefer native details and summary.

Challenge: Make #more reveal #extra and set aria-expanded to true, then reverse on the next click.

Hint: Update hidden and aria-expanded together.

```html
<button id="more" aria-expanded="false" aria-controls="extra">More details</button><div id="extra" hidden>Extra information</div>
```
```css
body { font: 16px/1.6 system-ui; margin: 24px; color: #233b46; }
button, input { font: inherit; padding: 8px; }

```
```js
document.querySelector("#more").addEventListener("click",e=>{const panel=document.querySelector("#extra");panel.hidden=!panel.hidden;e.currentTarget.setAttribute("aria-expanded",String(!panel.hidden));});
```

Manual review: Use the preview with the keyboard, then try a narrow viewport. Explain what you would check manually before publishing.

## 44. Filter visible records

A search interaction maps a query into visible results. Normalize text on both sides of the comparison. hidden is convenient for a small static list; larger datasets need broader design choices. Explain empty states when no record matches.

Challenge: Filter #items li by the case-insensitive text entered into #search.

Hint: Compare lowercase text, then set hidden for nonmatches.

```html
<label for="search">Search topics</label><input id="search"><ul id="items"><li>HTML</li><li>CSS</li><li>JavaScript</li></ul>
```
```css
body { font: 16px/1.6 system-ui; margin: 24px; color: #233b46; }
button, input { font: inherit; padding: 8px; }

```
```js
document.querySelector("#search").addEventListener("input",e=>{const q=e.target.value.toLowerCase();document.querySelectorAll("#items li").forEach(li=>li.hidden=!li.textContent.toLowerCase().includes(q));});
```

Manual review: Use the preview with the keyboard, then try a narrow viewport. Explain what you would check manually before publishing.

## 45. Delegate a repeated action

Event delegation uses one parent listener to handle events from several children. closest finds the relevant action element even when a nested span receives the click. Keep the match inside the intended container and avoid assuming every click is an action.

Challenge: Clicking a button in #actions should write its data-value to #selected.

Hint: Use closest to find the button from a nested target.

```html
<div id="actions"><button data-value="html"><span>HTML</span></button><button data-value="css">CSS</button></div><p id="selected"></p>
```
```css
body { font: 16px/1.6 system-ui; margin: 24px; color: #233b46; }
button, input { font: inherit; padding: 8px; }

```
```js
document.querySelector("#actions").addEventListener("click",e=>{const b=e.target.closest("button[data-value]");if(b)document.querySelector("#selected").textContent=b.dataset.value;});
```

Manual review: Use the preview with the keyboard, then try a narrow viewport. Explain what you would check manually before publishing.

## 46. Project: profile card

A useful profile card combines meaningful structure, readable text and a clear navigation action. Avoid making the whole card a confusing collection of overlapping click targets. Keep the heading and link purpose understandable without the visual design.

Challenge: Create an article.profile with an h2, paragraph and a link to #contact.

Hint: Combine the semantic elements you practiced earlier.

```html
<article class="profile"><h2>Maya Chen</h2><p>Front-end developer building helpful tools.</p><a href="#contact">Contact Maya</a></article><section id="contact"><h2>Contact</h2><p>Say hello.</p></section>
```
```css
body { font: 16px/1.6 system-ui; margin: 24px; color: #233b46; }
button, input { font: inherit; padding: 8px; }
.profile { padding:24px; border:1px solid #aac4ca; border-radius:16px; max-width:400px; }
```

Manual review: Use the preview with the keyboard, then try a narrow viewport. Explain what you would check manually before publishing.

## 47. Project: pricing cards

A pricing comparison needs clear plan names, costs and actions. Repeated markup benefits from a shared layout class. Keep the source order meaningful and allow the cards to wrap without hiding plan information.

Challenge: Build .plans with three article cards, each with h2 and button; use wrapping flex.

Hint: Give each plan a unique, descriptive button label.

```html
<div class="plans"><article><h2>Starter</h2><p>$0 / month</p><button>Choose Starter</button></article><article><h2>Builder</h2><p>$10 / month</p><button>Choose Builder</button></article><article><h2>Team</h2><p>$25 / month</p><button>Choose Team</button></article></div>
```
```css
body { font: 16px/1.6 system-ui; margin: 24px; color: #233b46; }
button, input { font: inherit; padding: 8px; }
.plans { display:flex; flex-wrap:wrap; gap:16px; } .plans article { flex:1 1 180px; padding:20px; background:#edf4f6; }
```

Manual review: Use the preview with the keyboard, then try a narrow viewport. Explain what you would check manually before publishing.

## 48. Project: FAQ page

Frequently asked questions work well when the question is the control label. Native disclosure elements give you keyboard interaction without custom event code. Allow answers to be read independently and do not hide essential page instructions inside them.

Challenge: Create three details elements, each with a summary and answer paragraph.

Hint: Build three independent native disclosures.

```html
<h1>Common questions</h1><details><summary>Where do I start?</summary><p>Start with HTML.</p></details><details><summary>Do I need an account?</summary><p>No account is needed here.</p></details><details><summary>Can I save my code?</summary><p>Download your page from the lab.</p></details>
```
```css
body { font: 16px/1.6 system-ui; margin: 24px; color: #233b46; }
button, input { font: inherit; padding: 8px; }
details { padding:12px; border-bottom:1px solid #ccd; }
```

Manual review: Use the preview with the keyboard, then try a narrow viewport. Explain what you would check manually before publishing.

## 49. Project: a live character counter

A small feedback widget combines a labeled field, a state-derived count and an accessible status. Decide what you are counting: JavaScript length counts UTF-16 code units, not every human-perceived character. Communicate limits accurately in real products.

Challenge: Make #bio update #count to its value.length followed by " characters".

Hint: Read value.length in the input handler.

```html
<label for="bio">Short bio</label><textarea id="bio"></textarea><p id="count" role="status">0 characters</p>
```
```css
body { font: 16px/1.6 system-ui; margin: 24px; color: #233b46; }
button, input { font: inherit; padding: 8px; }

```
```js
document.querySelector("#bio").addEventListener("input",e=>document.querySelector("#count").textContent=e.target.value.length+" characters");
```

Manual review: Use the preview with the keyboard, then try a narrow viewport. Explain what you would check manually before publishing.

## 50. Capstone: a small landing page

A complete page should communicate its purpose, guide the visitor to an action and remain usable at narrow widths. Combine semantic landmarks, a clear heading, responsive cards and a labeled contact form. Automated checks cover only selected requirements; manually review focus, contrast, copy and real content.

Challenge: Build header/nav, main with h1, three article cards in .features, a labeled email input in a form, and footer. Keep the page within the preview width.

Hint: Assemble the earlier patterns; the form stays local in this preview.

```html
<header><strong>Build Studio</strong><nav><a href="#features">Features</a> · <a href="#contact">Contact</a></nav></header><main><h1>Build something useful.</h1><p>Practice small skills. Make confident progress.</p><div class="features" id="features"><article><h2>Learn</h2><p>Understand the idea.</p></article><article><h2>Practice</h2><p>Try it in the browser.</p></article><article><h2>Build</h2><p>Connect the pieces.</p></article></div><form id="contact"><label for="email">Your email</label><input id="email" type="email" required><button type="submit">Join the demo</button></form><p id="thanks" role="status"></p></main><footer>DiscoveryVIP learning project</footer>
```
```css
body { font: 16px/1.6 system-ui; margin: 24px; color: #233b46; }
button, input { font: inherit; padding: 8px; }
.features { display:grid; grid-template-columns:repeat(auto-fit,minmax(min(100%,180px),1fr)); gap:16px; } article { padding:18px; background:#edf4f6; } form { margin-top:24px; } input { max-width:100%; box-sizing:border-box; }
```
```js
document.querySelector("form").addEventListener("submit",e=>{e.preventDefault();document.querySelector("#thanks").textContent="Demo only: no information was sent.";});
```

Manual review: Use the preview with the keyboard, then try a narrow viewport. Explain what you would check manually before publishing.
