What is the DOM?
The Document Object Model (DOM) is the bridge between JavaScript and a webpage. It is a structured, object-oriented representation of the HTML content that the browser creates when a page loads. The DOM treats the HTML document as a tree of nodes, where each HTML element becomes a JavaScript object.
Without the DOM, JavaScript would have no way to interact with the page. With the DOM, we can:
- Change text and attributes without reloading
- Show or hide elements based on user actions
- Create animations and effects
- Build interactive components (forms, sliders, etc.)
Examples from real sites — Amazon’s live cart count, social media comment updates, reaction buttons — all rely on JavaScript manipulating the DOM.
Exam tip: The DOM is language-independent (used by Python, Ruby, etc.), but in web development we interact with it purely through JavaScript in the browser.
DOM Tree Structure
Every time an HTML page is loaded, the browser builds a tree of objects. Each HTML element becomes a node; relationships between nodes (parent, child, sibling) define the tree’s hierarchy.
- The document object is the top-level entry point (parent of everything).
- Every node has properties like
children,parentNode,nextSibling. - Each node is a full JavaScript object with its own properties and methods.
The Document Object
The document object is created automatically by the browser when the page loads. It can be accessed directly in the console or in scripts.
console.log(document); // shows the HTML representation
console.dir(document); // shows the JavaScript object with all properties
console.dir(document) reveals a massive object with properties such as:
| Property | Description |
|---|---|
title | The page <title> text |
body | The <body> element object |
children | HTMLCollection of top-level children (usually <html>) |
all | All elements in the document |
URL | Current page URL |
anchors | Collection of anchor (<a>) elements |
head | The <head> element object |
forms | Collection of <form> elements |
These properties themselves are objects that can be drilled into with dot notation:
document.body.children // HTMLCollection of <header>, <main>, <script>, etc.
document.body.children[0] // first child of <body>
document.title // returns the page title string
Navigating the DOM
Because the DOM is a tree, you can traverse it step by step:
document.body.children // direct children of <body>
document.body.children[1] // second child (often <main>)
document.body.children[1].children[0] // first child of <main>
Each node also has properties for relationships:
parentNodefirstChildlastChildnextSiblingpreviousSibling
Exam tip:
childrenreturns only element nodes (ignoring text/whitespace). UsechildNodesto get all node types.
Manipulating Elements (Quick Examples)
Once you have a reference to a DOM node, you can change its content or style in real time.
Change text content:
document.body.children[1].children[0].innerText = "Haha you are hacked!";
Change background color:
document.body.style.backgroundColor = "yellow";
Change inner HTML (includes tags):
element.innerHTML = "<strong>New content</strong>";
These simple manipulations show the power of the DOM: no page reload needed.
Key takeaways
- The DOM is a tree of JavaScript objects representing the HTML structure.
documentis the entry point – everything starts from it.- Every element is an object with properties (e.g.,
title,body,children) and methods (e.g.,style,innerText). - Use
console.dir(document)to explore the live DOM object. - Navigate using dot notation and index-based
children. - Manipulation is immediate – change text, HTML, or CSS on the fly.
Connecting JavaScript to HTML
To manipulate a web page with JavaScript, you must first link the JS code to the HTML document. Without this connection, the browser cannot run your scripts or interact with the DOM.
Three ways to embed JavaScript
| Method | How it works | Best for | Pitfalls |
|---|---|---|---|
| Inline | Code placed directly inside an HTML element’s event attribute, e.g., onclick="alert('Hi')" | Quick tests of single-line actions | Mixes logic with markup; quotation mark conflicts; unreadable as project grows |
| Internal | Code written inside a <script> tag in the HTML file (in <head> or <body>) | Small projects or one-off scripts | Still couples JS with HTML; not reusable across pages |
| External | Code in a separate .js file, linked via <script src="file.js"> | All real-world projects | Requires correct file path and placement |
Exam tip: Never use inline JavaScript in production. It violates separation of concerns, becomes unmanageable, and confuses quote nesting — exactly like mixing inline CSS into HTML.
Where to place the <script> tag
Script placement determines when the JS code runs relative to the HTML elements it needs to interact with.
- Best practice: place the
<script>tag just before the closing</body>tag. This ensures all DOM nodes are created before JS tries to access them. - Result: no null-reference errors and faster perceived load time (HTML renders first, JS executes after).
Worked example: script in <head> vs. <body>
Given HTML with <h1>Original</h1> and a script that tries to change it:
<!-- BAD: script in <head> -->
<head>
<script>
document.querySelector('h1').innerText = 'Changed'; // runs before <h1> exists → null error
</script>
</head>
<!-- GOOD: script at bottom of <body> -->
<body>
<h1>Original</h1>
<!-- ... -->
<script>
document.querySelector('h1').innerText = 'Changed'; // works fine
</script>
</body>
Key takeaways
- Inline JS is quick but messy — avoid beyond one-liners.
- External JS (separate
.jsfile) is the cleanest, most reusable method. - Place
<script>at the bottom of<body>to guarantee all elements are loaded. - Placing scripts in
<head>frequently causesnullerrors when the script attempts to access DOM elements that don't exist yet.
Selecting DOM Elements
Before you can modify a web page, you must select the element(s) you want to interact with. JavaScript provides several methods on the global document object to do this.
Old-style selectors (still valid but less flexible)
| Method | Returns | Example | Notes |
|---|---|---|---|
document.getElementById('id') | Single element | document.getElementById('main-paragraph') | IDs must be unique; fast |
document.getElementsByClassName('class') | HTMLCollection (live, array-like) | document.getElementsByClassName('main-div') | Index with [0] to get first |
document.getElementsByTagName('tag') | HTMLCollection | document.getElementsByTagName('li') | Use index to access specific item |
HTMLCollection is a live collection – it updates automatically when the DOM changes. It does not have forEach or other array methods (though you can convert it with Array.from).
Modern, powerful selector: querySelector and querySelectorAll
These methods accept any CSS selector string, making them far more flexible.
| Method | Returns | Example |
|---|---|---|
document.querySelector(selector) | First matching element (or null) | document.querySelector('.main-div') |
document.querySelectorAll(selector) | NodeList (static, array-like) | document.querySelectorAll('li img') |
NodeList is static (does not update when DOM changes) and supports forEach directly.
CSS selector examples (same as in stylesheets)
// By ID (use #)
document.querySelector('#main-paragraph')
// By class (use .)
document.querySelector('.main-div')
// By tag name
document.querySelector('h1')
// Descendant
document.querySelectorAll('.main-div img')
// Complex: image inside an li
document.querySelectorAll('li img')
Exam tip: Prefer
querySelectorandquerySelectorAllover old methods. They are more powerful, easier to remember (use CSS syntax you already know), and cover every selection scenario.
Assigning selections to variables
Once selected, you can store the element reference in a variable and reuse it.
const paragraph = document.getElementById('main-paragraph');
paragraph.innerText = 'Modified text';
paragraph.style.backgroundColor = 'brown';
Why use const for DOM elements?
Variables assigned to objects (including DOM elements) store a reference to the object, not a copy. Using const prevents accidental reassignment of the variable to a different element, but still allows you to change the element's properties.
const arr = [1, 2];
arr[0] = 23; // allowed – array content changes
// arr = [3,4]; // error – cannot reassign a const
const heading = document.querySelector('h1');
heading.innerText = 'New'; // allowed
// heading = somethingElse; // error – keeps reference stable
Key difference: HTMLCollection vs. NodeList
getElementsByClassName/getElementsByTagName→ HTMLCollection (live, noforEach)querySelectorAll→ NodeList (static, hasforEach)
Worked example: changing the first image’s alt text
const images = document.querySelectorAll('img'); // NodeList
images[0].alt = 'ha ha ha'; // change alt of first image
Key takeaways
- Select by ID with
getElementByIdorquerySelector('#id')– always returns a single element. - Select by class/tag with
getElementsByClassName/getElementsByTagNamereturns an HTMLCollection. - Best practice: use
querySelector(first match) andquerySelectorAll(all matches) for any CSS selector. - Store selections in variables – prefer
constfor stability (the element reference is fixed, properties remain changeable). - All selection methods are called on
document(e.g.,document.querySelector()).
DOM Traversing
DOM traversal is the ability to navigate the relationships between elements in the DOM tree — moving to an element's parent, children, or siblings. Think of the DOM as a family tree: each element is a node connected to others. Traversal methods let you target elements based on their position, essential for dynamic web interactions.
Parent Access
parentElement– returns the immediate parent HTML element of the selected element.- Chaining
.parentElementrepeatedly climbs the tree:element.parentElement.parentElementgives the grandparent.
- Chaining
const h2 = document.querySelector('h2');
h2.parentElement; // <section>
h2.parentElement.parentElement; // <main>
Note: The root of the tree is
document; its parent isnull.
Children Access
children– returns a liveHTMLCollectionof only HTML child elements (ignores text nodes, comments).- To get all child nodes including text, use
childNodes(rarely needed for traversal).
- To get all child nodes including text, use
const body = document.querySelector('body');
body.children; // [<header>, <main>, <script>, ...]
firstElementChild– first child element.lastElementChild– last child element.childElementCount– number of child elements (equivalent tochildren.length).
body.firstElementChild; // <header>
body.lastElementChild; // <script>
body.childElementCount; // 3 (example)
Sibling Access
nextElementSibling– the next sibling element (following the same parent).previousElementSibling– the previous sibling element.
Chaining these moves horizontally through the tree.
const divMain = document.querySelector('.main');
divMain.nextElementSibling; // <p>
divMain.nextElementSibling.nextElementSibling; // <button>
divMain.previousElementSibling; // null (if first child)
Visualising Traversal
Worked Example: Climbing the Tree
Given this HTML structure:
<body>
<header>
<div class="main">...</div>
<p>...</p>
<button>...</button>
</header>
<main>
<section>...</section>
<section>...</section>
</main>
</body>
// Start with the first <li> inside a list
const li = document.querySelector('li');
li.parentElement; // <ul>
li.parentElement.parentElement; // <section> or parent container
li.children; // [<br>, <img>] (HTMLCollection)
li.nextElementSibling; // next <li> (if any)
li.previousElementSibling; // previous <li> (if any)
Property Summary
| Property | Returns | Use |
|---|---|---|
parentElement | Single element | Climb one level up |
children | Live HTMLCollection | All child elements |
firstElementChild | Single element | First child |
lastElementChild | Single element | Last child |
childElementCount | Number | Count children |
nextElementSibling | Single element | Move forward |
previousElementSibling | Single element | Move backward |
Exam tip:
childrenreturns only elements, not text nodes. If you need all nodes (including whitespace), usechildNodes. But for traversal, always prefer the element-specific properties — they are robust and what exam questions expect.
Key Takeaways
parentElementclimbs up;children,firstElementChild,lastElementChildgo down.nextElementSibling/previousElementSiblingmove sideways among siblings.childElementCountgives the number of child elements quickly.- Chaining these methods navigates the tree in any direction (up, down, sideways).
- All methods return
nullor empty collections when nothing exists — always check before further chaining.
Modifying Styles and Attributes with JavaScript
JavaScript + the DOM lets you change how a page looks and behaves on the fly — reacting to user actions, time, or any condition. At its core: you grab an element and rewrite its visual presentation or its HTML attributes.
The style Property – Direct Inline Changes
Every DOM element has a .style property that reflects its inline styles. You can read or set any CSS property, but the property names must be written in camelCase (not hyphenated):
| CSS property | JavaScript .style access |
|---|---|
background-color | element.style.backgroundColor |
font-family | element.style.fontFamily |
font-size | element.style.fontSize |
border | element.style.border |
Example – turn the navbar background blue:
const navBar = document.querySelector('nav');
navBar.style.backgroundColor = 'blue';
Values are always strings; include units where needed:
navBar.style.border = '2px solid black';
Limitation:
.styleonly sees and modifies inline styles — it ignores styles from external or internal CSS sheets. If you try to read a property that comes from a stylesheet, you getnull.
Why it’s problematic – Mixes presentation (CSS) with logic (JS), harder to maintain, and cannot read the full computed style of an element.
The classList API – The Right Way to Style
Instead of writing inline styles, define your styles in CSS classes and toggle them with JavaScript. The classList property gives you a DOMTokenList (array-like object) of all classes on an element.
Key methods:
| Method | Effect |
|---|---|
element.classList.add('class-name') | Adds a class |
element.classList.remove('class-name') | Removes a class |
element.classList.toggle('class-name') | Adds if absent, removes if present |
element.classList.contains('class-name') | Returns true/false |
Example workflow:
.sample {
color: green;
background-color: red;
text-decoration: solid;
}
const section = document.querySelector('section');
section.classList.add('sample'); // applies the green/red style
section.classList.remove('sample'); // removes it
section.classList.toggle('sample'); // toggles
console.log(section.classList.contains('sample')); // false (after removal)
Exam tip: Always prefer
classListover.stylefor production code. It keeps styles in CSS, makes debugging easier, and lets you reuse the same class across many elements.
Working with Other Attributes
Every HTML attribute (e.g., href, src, id, alt) can be read or changed using two generic methods:
element.getAttribute('attribute-name')– returns the current value.element.setAttribute('attribute-name', 'new-value')– sets the value (overwrites existing).
Example – change the first link’s href:
const firstLink = document.querySelector('a');
console.log(firstLink.getAttribute('href')); // e.g., "events.html"
firstLink.setAttribute('href', 'https://govind.com');
firstLink.setAttribute('id', 'new-id'); // also works for any attribute
Warning: Using
setAttribute('style', ...)overwrites ALL inline styles – avoid it for styling. UseclassListinstead.
Text Content: innerText , textContent , innerHTML
Three properties control what text (and HTML) lives inside an element. They differ in what they read and how they treat markup.
| Property | What it returns | Security concern | Use case |
|---|---|---|---|
innerText | Visible text only (respects CSS display:none) | Safe | Reading or setting plain visible text |
textContent | All raw text, including hidden text and whitespace | Safe | When you need the full raw string (e.g., for comparison) |
innerHTML | Text plus any nested HTML tags; parses them as elements | Dangerous with user input (XSS) | When you must insert structured HTML (only with trusted content) |
Example – given a list item <li>Only one guest allowed <br> <img src="..."></li>:
const li = document.querySelector('li');
console.log(li.innerText); // "Only one guest allowed" (no br, no image text)
console.log(li.textContent); // "Only one guest allowed \n " (includes newline)
console.log(li.innerHTML); // "Only one guest allowed <br> <img src="...">"
// Setting:
li.innerText = 'Any number of guests'; // Replaces everything (including br/img) with plain text
li.innerHTML = '<span>Hi</span>'; // Inserts a <span> element
Exam tip: Never use
innerHTMLwith user-generated input (comments, search queries). UseinnerTextortextContentto avoid cross-site scripting (XSS) vulnerabilities.
Key takeaways – Styles & Attributes
.stylegives access to inline styles only; property names use camelCase.classListis the preferred way to style via JS –.add(),.remove(),.toggle(),.contains().- Use
getAttribute()andsetAttribute()for any HTML attribute exceptstyle. setAttribute('style', ...)overrides all inline styles – avoid.
Key takeaways – Text Content
innerText– visible text only.textContent– all raw text, including hidden.innerHTML– parses HTML; only use with trusted content.- Setting any of these replaces all children of the element.
DOM Manipulation: Creating, Appending, Removing, and Inserting Elements
DOM manipulation lets you dynamically add, remove, or rearrange elements on a page. Instead of hard‑coding everything in HTML, you use JavaScript to modify the DOM tree at runtime – the engine behind interactive apps and single‑page interfaces.
Creating Elements
Before you can insert an element, you must create it. The method document.createElement(tagName) returns a new, empty DOM node of the given tag (e.g., 'p', 'div', 'span'). It exists only in memory until attached to the page.
Example
const newParagraph = document.createElement('p'); newParagraph.innerText = 'This P was added with JavaScript';
The element is now a node object with an innerText property, but it is not yet visible in the document.
Appending Elements
Once created, an element must be attached to a parent node. Two primary methods exist: appendChild() and the newer, more flexible append().
appendChild(parentNode, childNode)
- Appends one child node to the end of the parent’s children list.
- The child must be a node (e.g., a
pelement created withcreateElement). - Cannot insert text strings directly.
const section1 = document.querySelector('section');
section1.appendChild(newParagraph);
append(parentNode, ...nodesOrStrings)
- Adds one or more children or strings to the end of the parent.
- Strings are converted to text nodes automatically – they are not parsed as HTML.
- More versatile than
appendChild.
const newPara2 = document.createElement('p');
newPara2.innerText = 'Second dynamic paragraph';
section1.append(newPara2, 'Some random text', '<span>this is treated as text, not HTML</span>');
| Method | Accepts multiple children? | Accepts text strings? | Returns |
|---|---|---|---|
appendChild | No (one node at a time) | No | The appended child node |
append | Yes | Yes (as text) | undefined |
Removing Elements
Two ways to delete an element from the DOM: directly on the element itself, or via its parent.
remove() on the Target Element
Simply call element.remove(). No parent reference needed – the element is detached from the DOM.
const listItem1 = document.querySelector('li'); // first <li>
listItem1.remove(); // gone from the page
removeChild() on the Parent Element
Requires both the parent and the child node you want to remove.
const ul = document.querySelector('ul');
const firstChild = ul.firstElementChild;
ul.removeChild(firstChild);
Key difference: remove() is cleaner when you already have a reference to the element; removeChild() forces you to know the parent.
Inserting Elements at Specific Positions
Appending always puts a child at the end. To control exactly where an element appears, use insertBefore() or insertAdjacentElement().
insertBefore(parentNode, newNode, referenceNode)
- Inserts
newNodebeforereferenceNode(which must be a child of the parent). referenceNodeis the sibling before which the new node goes.
const ul = document.querySelector('ul');
const listItem2 = ul.children[1]; // second <li>
const newP = document.createElement('p');
newP.innerText = 'Inserted before second list item';
ul.insertBefore(newP, listItem2);
insertAdjacentElement(position, targetElement, newElement)
- Inserts
newElementat a position relative totargetElement– no parent required. - Four position strings (case‑sensitive):
| Position | Meaning |
|---|---|
'beforebegin' | Before the target element itself (outside) |
'afterbegin' | Inside the target, before its first child |
'beforeend' | Inside the target, after its last child |
'afterend' | After the target element itself (outside) |
Example – inserting before and inside:
const li1 = document.querySelector('#adjacent li:first-child');
const pBefore = document.createElement('p');
pBefore.innerText = 'Added beforebegin';
li1.insertAdjacentElement('beforebegin', pBefore);
const li2 = document.querySelector('#adjacent li:nth-child(2)');
const pAfterBegin = document.createElement('p');
pAfterBegin.innerText = 'Added afterbegin';
li2.insertAdjacentElement('afterbegin', pAfterBegin);
Comparison of insertion methods:
| Method | Uses parent? | Positions | Can insert multiple nodes? |
|---|---|---|---|
append / appendChild | Yes | Only end of children | Only append can do multiple |
insertBefore | Yes | Before a specified child | No |
insertAdjacentElement | No (relative to an element) | Four exact positions | No (but call repeatedly) |
Exam tip:
insertAdjacentElementis the most flexible for precise positioning. Remember the four position strings – they are often tested. Note that'beforeend'is inside the element at the end, while'beforebegin'is outside before it.
Key Takeaways
- Use
document.createElement(tagName)to create a new element in memory. appendChild()appends a single node to a parent;append()can add multiple nodes and text strings.element.remove()deletes the element directly;parent.removeChild(child)needs both references.insertBefore(parent, new, ref)places a node before a sibling child.insertAdjacentElement(position, target, new)gives four precise placement options relative to the target element.- Always ensure the target elements exist in the DOM before manipulating them – check for
nullif usingquerySelector.
Event Listeners
Events are signals sent by the browser to JavaScript that something has happened – a user clicked, typed, hovered, or a page finished loading. They are the core mechanism behind interactivity: without events, a webpage is static; with events, JavaScript can react and update the DOM.
How the browser handles events (the flow)
- User triggers an action – e.g., clicks a button, presses a key, scrolls.
- Browser detects the action – creates an event and fires it on the target element.
- JavaScript responds – runs code attached to that event (the listener).
Intuition: Think of events as “when something happens, do something else.” The browser shouts “Hey, button clicked!”, and your JavaScript wakes up and runs the code you wrote for that moment.
Three ways to attach event handlers
| Method | Example | Profile |
|---|---|---|
| Inline HTML (old, messy) | <button onclick="alert('Clicked!')"> | Hard to maintain, mixes HTML & JS, avoid |
| DOM property (better, still not ideal) | button.onclick = function() { … } | Only one handler per event type; overwrites previous |
addEventListener (gold standard) | button.addEventListener('click', handler) | Multiple handlers per event, clean separation, modern |
Exam tip: Interviewers love to ask why
addEventListeneris preferred over the other two. Always mention: it allows multiple listeners, works with all event types uniformly, and doesn’t override existing handlers.
Inline example (don’t use)
<button onclick="alert('I was clicked')">Click me</button>
Property example (not recommended)
const button = document.querySelector('button');
button.onclick = function() {
alert('I was clicked');
};
This works, but if you later assign another onclick, it replaces the first.
addEventListener – the modern way
Syntax:
element.addEventListener(type, listener);
type– a string naming the event (e.g.'click','mouseover','keydown').listener– a callback function that runs when the event occurs. This function automatically receives an event object as its first argument.
Worked example (background colour change)
Given a <button> in the HTML:
<button type="button">Change background</button>
Step 1 – Define the handler function (using classList is best practice):
function changeBg() {
document.body.classList.add('bg-blue');
}
With CSS:
.bg-blue { background-color: blue; }
Step 2 – Attach the listener:
const button = document.querySelector('button');
button.addEventListener('click', changeBg);
Now clicking the button applies the class.
Alternative – anonymous function (also works):
button.addEventListener('click', function() {
document.body.style.backgroundColor = 'cyan';
});
Alternative – arrow function (modern):
button.addEventListener('click', () => {
document.body.style.backgroundColor = 'cyan';
});
Exam tip: Named functions are more readable and reusable; anonymous functions are convenient for one-off logic. Arrow functions do not have their own
this(important for certain contexts like object methods).
The Event Object
When the listener runs, JavaScript automatically passes an event object as the first argument to the callback. You can name it anything (common names: e, event, evt).
button.addEventListener('click', function(e) {
console.log(e); // entire event object (e.g., PointerEvent)
console.log(e.target); // the element that triggered the event
console.log(e.currentTarget); // the element the listener is attached to
});
e.target– the element that was actually clicked (useful if you have nested elements).e.currentTarget– the element the listener is attached to (often the same, but differs with event delegation).
Common event types (selected)
| Category | Events | When fired |
|---|---|---|
| Mouse | click, dblclick, mouseover, mouseout, mousedown, mouseup, contextmenu (right-click) | Cursor interactions |
| Keyboard | keydown, keyup, keypress (deprecated) | Key presses |
| Form | submit, change, input, focus, blur | Form field interactions |
| Document / Window | DOMContentLoaded, load, scroll, resize | Page lifecycle |
| Touch | touchstart, touchend, touchmove | Touchscreen devices |
A full reference is maintained on MDN: Event reference.
Exam tip: For mouse events, remember
click=mousedown+mouseupon the same element. For keyboard events,keydownfires repeatedly while holding a key;keyupfires once on release.
Key takeaways
- An event is a signal that something happened; event listeners let JavaScript react.
- Three attachment methods exist;
addEventListeneris the gold standard. addEventListener(type, listener)–typeis a string,listeneris a callback.- The callback receives an event object providing details (e.g.,
e.target). - Named functions, anonymous functions, and arrow functions all work as listeners.
- Always separate JS from HTML; never use inline
onclickattributes in production.
Painting Website
This project ties together HTML, CSS, and JavaScript (DOM manipulation, events, loops) to build an interactive painting canvas. The goal: hover over a grid of pixel‑divs and color them, but only while holding the Alt key — simulating a brush. The project also demonstrates event delegation for performance when handling thousands of child elements.
1. HTML Structure
Minimal markup: a color input, a paragraph of instructions, and a container <div> that will hold the pixel grid. All pixels are created dynamically with JavaScript.
<h1>Painting Canvas</h1>
<label for="color">Select Color</label>
<input type="color" id="color" value="#000000">
<p>Press Alt + move over canvas to paint</p>
<div class="container"></div>
2. CSS Grid Canvas
The container is turned into a responsive grid. Each grid cell is a “pixel” — a <div> with class .pixel. The grid uses 1fr columns so the canvas scales.
.container {
border: 2px solid black;
display: grid;
grid-template-columns: repeat(150, 1fr);
width: 75vw;
height: 60vh;
}
Why 150 columns? Creates small square cells. With 5000 total pixels, we get about 30 rows.
3. JavaScript: Creating Pixels Dynamically
A loop generates 5000 <div> elements, each with a unique id and class pixel. They are appended to the container.
const canvas = document.querySelector('.container');
for (let i = 0; i < 5000; i++) {
const pixel = document.createElement('div');
pixel.setAttribute('id', i + 1);
pixel.setAttribute('class', 'pixel');
canvas.appendChild(pixel);
}
4. Adding Event Listeners (Inefficient Approach)
The naive way: attach a mouseover listener to every pixel inside the loop. When the event fires, a function runs that:
- Checks if the Alt key was pressed (
event.altKey). - Reads the current value of the color input (
document.querySelector('#color').value). - Sets the
background-colorof the pixel that triggered the event (usingevent.currentTarget).
pixel.addEventListener('mouseover', function(e) {
if (e.altKey) {
const color = document.querySelector('#color').value;
e.currentTarget.style.backgroundColor = color;
}
});
Why
e.currentTarget? It refers to the element on which the listener was attached — in this case, the individual pixel.e.targetwould also work here because there’s no nesting inside the pixel.
5. The Better Way: Event Delegation
Attaching 5000 listeners is wasteful. Instead, use event bubbling — set a single listener on the parent container. Events that occur on any child pixel bubble up to the container. Then inside the handler, use e.target to identify the actual pixel that was hovered.
canvas.addEventListener('mouseover', function(e) {
if (e.altKey && e.target.classList.contains('pixel')) {
const color = document.querySelector('#color').value;
e.target.style.backgroundColor = color;
}
});
Event Bubbling vs. Capturing
- Events bubble from the target element up through its ancestors.
e.target= the element that originally triggered the event (the pixel).e.currentTarget= the element with the event listener (the container, in the delegation version).
Exam tip: Event delegation is the standard way to handle events on many dynamic children. It saves memory and works even for elements added after the listener is attached.
6. Conditional Painting (Alt Key)
Only color a pixel when the Alt key is held down. This prevents accidental scribbling. The property e.altKey is true if the Alt key was pressed when the event fired.
if (e.altKey) {
// apply color
}
7. Worked Example (Step‑by‑Step Flow)
- User selects a color (e.g., green) from the
<input type="color">. - User holds the Alt key and moves the mouse over the canvas.
- A
mouseoverevent fires on a pixel div. - The event bubbles to the container, which has the listener.
- The handler checks:
e.altKey === trueande.targetis a.pixel. - It reads
document.querySelector('#color').value(which is"#00ff00"at that moment). - It sets
e.target.style.backgroundColor = "#00ff00".
8. Key Concepts Summarised
| Concept | Explanation |
|---|---|
Event object (e) | Contains properties like target, currentTarget, altKey, button. |
e.target | The element on which the event originally occurred (the pixel). |
e.currentTarget | The element on which the listener was attached (container in delegation). |
| Event bubbling | Default behaviour where an event propagates from child to parent elements. |
| Event delegation | Technique of listening on a parent for events that bubble from children, improving performance and allowing dynamic elements. |
9. Connection to Previous Modules
- HTML/CSS: Grid layout, selectors, styling.
- Module 6 (JS Basics): variables, loops (
for), functions, callback functions (used as event handlers). - Module 7: DOM selection (
querySelector), element creation (createElement), attribute setting (setAttribute), appending children, modifying inline styles.
Key Takeaways
- Use a
<div>container with CSS grid to create a pixel canvas. - Generate grid cells dynamically with a loop to avoid hard‑coding HTML.
- Apply color only when a modifier key (e.g., Alt) is held — check
event.altKey. - Event delegation (listener on the parent) is far more efficient than attaching listeners to every child.
e.target(the originated element) vse.currentTarget(the listener’s element) is a common exam distinction.- This project integrates loops, functions, DOM manipulation, and events — typical of interactive web pages.
Overview: The Typewriter Effect
The goal is to display sentences one by one, each typed out character by character, then erased character by character before the next sentence appears. A blinking cursor sits at the end of the typed text. The animation starts automatically when the page loads — no user interaction required.
The key insight: this is a continuous loop that must not block the browser. We achieve it by combining recursive functions (a function calling itself with a delay) and JavaScript’s asynchronous timing methods (setTimeout, setInterval).
The Building Blocks
| Component | HTML Element | Purpose | CSS Class Used |
|---|---|---|---|
| Static prefix | <span> (e.g. "IIMB") | Always visible | — |
| Animated text | <span id="animated"> | Holds the typed/erased string | — |
| Blinking cursor | <span id="cursor"> | Vertical bar (` | `) that toggles visibility |
Delay functions in JavaScript
setTimeout(fn, delay)– Executesfnonce afterdelaymilliseconds.setInterval(fn, delay)– Executesfnrepeatedly everydelaymilliseconds.
Both accept a callback function as an argument. They do not block the main thread — they run asynchronously. Without this, an infinite loop would freeze the page.
Step-by-step Construction
1. Blinking Cursor with setInterval
const cursor = document.querySelector('#cursor');
function toggleCursor() {
cursor.classList.toggle('hidden'); // toggles display: none
}
setInterval(toggleCursor, 500); // blink every 0.5s
- The
.hiddenclass setsdisplay: none. setIntervalkeeps callingtoggleCursor→ cursor appears and disappears.
2. Wait for DOM to be ready
document.addEventListener('DOMContentLoaded', startAnimation);
- The event
DOMContentLoadedfires when the HTML is fully parsed. - Only then do we start the animation (calling
startAnimation, which triggers both cursor blinking and the typewriter).
3. Typewriter Logic: Recursion with setTimeout
The problem: a regular for loop would print all characters instantly — no delay.
Solution: use a recursive function that calls itself via setTimeout.
Data
sentences = ["Lay the foundation to web development.", "The best institute in the world.", "Learn to live your life happily and keep being curious."]sentenceCounter(0..2 → cycles with modulo)charCounter(0..length-1)
State diagram (conceptual)
Key implementation details
- Printing:
animatedSpan.textContent += sentence[sentenceCounter][charCounter] - Erasing:
animatedSpan.textContent = animatedSpan.textContent.slice(0, -1) textContent(notinnerText) preserves spaces correctly.- Use
setTimeoutinside the recursive call — neversetIntervalfor one-shot steps. UsingsetIntervalhere causes infinite rapid re‑calls.
4. Achieving the infinite loop with modulo
When a sentence is fully erased, we need to move to the next sentence:
sentenceCounter = (sentenceCounter + 1) % sentences.length;
%(modulo) keeps the counter within0..2, wrapping back to 0 after the last sentence.
Worked example: First iteration
Initial state: sentenceCounter = 0, charCounter = 0, sentences[0] = "Lay..."
-
addCharactersruns:charCounter (0) < "Lay...".length→ true- Append
sentences[0][0]→"L" charCounter++→ 1setTimeout(addCharacters, 100)
-
After 100ms:
- Append
"a"→"La" charCounter = 2setTimeout(addCharacters, 100)
- Append
… continues until charCounter reaches 37 (length of first sentence).
-
Then:
charCounter >= length→setTimeout(removeCharacters, 2000)→ pause 2 seconds
-
removeCharacters:charCounter = 37> 0- Slice off last char →
"Lay ... developmen" charCounter--→ 36setTimeout(removeCharacters, 20)
… fast removal at 20ms per character until charCounter = 0.
- When
charCounter = 0:sentenceCounter = (0 + 1) % 3 = 1→ next sentencecharCounter = 0setTimeout(addCharacters, 100)
The cycle repeats forever.
Asynchronous behaviour
setTimeoutandsetIntervalare non‑blocking. The JavaScript event loop schedules them; the main thread continues running other code (e.g. user clicks).- Without asynchronous delay, an infinite loop would freeze the browser tab.
Exam tip: The difference between
setTimeout(one‑shot) andsetInterval(repeated) is a common trick question. In recursive animation, usesetTimeoutinside the function to create a controlled delay between steps. Confusing them leads to bugs such as a “super fast” loop.
Connecting the pieces
The final animation runs two independent async loops:
- Cursor blink –
setInterval, simple toggle every 500ms. - Typewriter – recursive
setTimeoutchain for add/erase, with state updated via counters and modulo.
Both are started synchronously inside startAnimation(), triggered by DOMContentLoaded.
Key takeaways
- Typewriter animations are built character‑by‑character using recursive
setTimeout, not synchronous loops. setIntervalis appropriate for repeated actions (blinking);setTimeoutfor one‑shot delays inside recursion.- Modulo arithmetic (
%) elegantly cycles through an array without extra checks. textContentcorrectly handles whitespace;innerTextsometimes omits it.- Asynchronous timing keeps the page responsive — the animation does not block user interactions.
- Professional result achieved with just a few concepts: DOM selection, class toggle, string slicing, counters, and delay functions.