Skip to main content

🎯 Lesson 14: Selecting & Modifying Elements

Now that you understand the DOM tree, it's time to reach in and grab elements — then change their text, classes, styles, and attributes with JavaScript.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Select single elements with querySelector and getElementById
  • Select multiple elements with querySelectorAll
  • Read and change element text with textContent and innerHTML
  • Add, remove, and toggle CSS classes with classList
  • Modify inline styles with the style property
  • Get and set HTML attributes with getAttribute and setAttribute
  • Work with data attributes (data-*)

Estimated Time: 50 minutes

Project: Build a dynamic profile card editor

📑 In This Lesson

Selecting a Single Element

querySelector() — The Modern Standard

querySelector finds the first element matching a CSS selector. If you know CSS selectors, you already know how to use it.


// By tag name
const heading = document.querySelector("h1");

// By class name (prefix with .)
const card = document.querySelector(".card");

// By ID (prefix with #)
const nav = document.querySelector("#main-nav");

// By attribute
const link = document.querySelector('a[href="#intro"]');

// Complex selectors work too
const firstItem = document.querySelector("ul.todo-list > li:first-child");

// Returns null if nothing matches
const nope = document.querySelector(".nonexistent");
console.log(nope);  // null
                

getElementById() — The Classic


// Only selects by ID — no # prefix needed
const nav = document.getElementById("main-nav");

// Equivalent to:
const nav2 = document.querySelector("#main-nav");
                

💡 querySelector vs. getElementById

Use querySelector for everything — it's more flexible since it accepts any CSS selector. getElementById is slightly faster for ID lookups, but the difference is negligible in practice. Consistency wins.

Selecting Multiple Elements

querySelectorAll()

querySelectorAll returns all matching elements as a NodeList. You can loop over it with forEach or for...of.


// Get all paragraphs
const paragraphs = document.querySelectorAll("p");
console.log(paragraphs.length);  // Number of <p> elements

// Loop with forEach
paragraphs.forEach(p => {
    console.log(p.textContent);
});

// Loop with for...of
for (const p of paragraphs) {
    p.style.color = "blue";
}

// Get all elements with a specific class
const cards = document.querySelectorAll(".card");

// Complex selectors
const activeLinks = document.querySelectorAll("nav a.active");
                

⚠️ NodeList Is Not an Array

A NodeList supports forEach and for...of, but not array methods like map, filter, or reduce. Convert it to an array if you need those.


const items = document.querySelectorAll("li");

// ❌ items.map(...) — TypeError!

// ✅ Convert to array first
const itemArray = [...items];  // Spread into array
const texts = itemArray.map(item => item.textContent);

// Or use Array.from()
const texts2 = Array.from(items).map(item => item.textContent);
                    

Scoped Queries

You can call querySelector on any element, not just document. This limits the search to that element's descendants.


const sidebar = document.querySelector(".sidebar");

// Only search within the sidebar
const sidebarLinks = sidebar.querySelectorAll("a");
const sidebarTitle = sidebar.querySelector("h2");
                
graph TD A["Selecting Elements"] --> B["querySelector()"] A --> C["querySelectorAll()"] A --> D["getElementById()"] B --> E["First match
or null"] C --> F["All matches
(NodeList)"] D --> G["By ID only
or null"] style B fill:#ecfdf5,stroke:#22c55e,stroke-width:2px,color:#1e293b style C fill:#eff6ff,stroke:#3b82f6,stroke-width:2px,color:#1e293b style D fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,color:#1e293b

Changing Text Content

textContent — Safe and Simple

textContent gets or sets the plain text inside an element. It's the safest option because it treats everything as text (no HTML parsing).


const heading = document.querySelector("h1");

// Read the text
console.log(heading.textContent);  // "Hello World"

// Change the text
heading.textContent = "New Heading!";

// HTML tags are treated as plain text (safe!)
heading.textContent = "<em>Not italic</em>";
// Displays literally: <em>Not italic</em>
                

innerHTML — Parses HTML

innerHTML gets or sets the HTML content inside an element. It parses HTML tags, which is powerful but has security implications.


const container = document.querySelector(".content");

// Read the HTML
console.log(container.innerHTML);  // "<p>Hello</p><p>World</p>"

// Set new HTML — tags are parsed and rendered
container.innerHTML = "<h2>New Section</h2><p>With a paragraph.</p>";

// Add to existing HTML
container.innerHTML += "<p>Another paragraph</p>";
                

❌ Never Use innerHTML with User Input

If you insert user-provided text with innerHTML, a malicious user could inject JavaScript code. This is called a Cross-Site Scripting (XSS) attack. Always use textContent for user data.


const userInput = '<img src="x" onerror="alert(\'hacked!\')">';

// ❌ DANGEROUS — this would execute the script
// element.innerHTML = userInput;

// ✅ SAFE — treats everything as plain text
element.textContent = userInput;
                    
Property Parses HTML? Safe for User Input? Use When
textContentNoYesSetting or reading plain text
innerHTMLYesNo!Inserting HTML you control
innerTextNoYesReading visible text only (slower)

Working with Classes

The classList property provides methods to add, remove, toggle, and check CSS classes. This is how you change an element's appearance dynamically.


const card = document.querySelector(".card");

// Add a class
card.classList.add("highlighted");
// <div class="card highlighted">

// Remove a class
card.classList.remove("highlighted");
// <div class="card">

// Toggle — add if missing, remove if present
card.classList.toggle("active");
// If it had "active" → removes it. If it didn't → adds it.

// Check if a class exists
if (card.classList.contains("card")) {
    console.log("It's a card!");  // true
}

// Add multiple classes at once
card.classList.add("featured", "new", "sale");

// Remove multiple classes
card.classList.remove("new", "sale");

// Replace one class with another
card.classList.replace("featured", "standard");
                

Practical: Toggle Dark Mode


const toggleButton = document.querySelector("#theme-toggle");

toggleButton.addEventListener("click", () => {
    document.body.classList.toggle("dark-mode");
});

// In CSS:
// .dark-mode { background: #1a1a2e; color: #eee; }
                

⚠️ classList vs. className

The older className property returns all classes as a single string. Modifying it replaces all classes. Use classList instead — it's safer and more precise.


// ❌ className replaces everything
card.className = "active";  // Removes "card" class too!

// ✅ classList adds/removes individually
card.classList.add("active");  // Keeps existing classes
                    

Modifying Inline Styles

The style property lets you set inline CSS on an element. Property names use camelCase instead of kebab-case.


const box = document.querySelector(".box");

// Set individual styles
box.style.backgroundColor = "#3b82f6";
box.style.color = "white";
box.style.padding = "1rem";
box.style.borderRadius = "8px";
box.style.fontSize = "18px";

// Read a style
console.log(box.style.color);  // "white"

// Remove a style (set to empty string)
box.style.backgroundColor = "";
                
CSS Property JavaScript style Property
background-colorbackgroundColor
font-sizefontSize
border-radiusborderRadius
z-indexzIndex
margin-topmarginTop

Getting Computed Styles

element.style only reads inline styles. To see the actual applied styles (including CSS from stylesheets), use getComputedStyle.


const heading = document.querySelector("h1");

// Only reads inline styles (often empty)
console.log(heading.style.fontSize);  // "" (not set inline)

// Reads the ACTUAL computed style (from CSS + inline)
const computed = getComputedStyle(heading);
console.log(computed.fontSize);      // "32px" (from stylesheet)
console.log(computed.color);         // "rgb(30, 41, 59)"
                

✅ Prefer Classes Over Inline Styles

For most visual changes, toggle a CSS class instead of setting inline styles. Classes keep your styling in CSS where it belongs. Use inline styles only for dynamic values (like calculated positions or user-chosen colors).


// ✅ Better — toggle a class
element.classList.add("highlighted");

// ❌ Avoid for static styling
element.style.backgroundColor = "yellow";
element.style.fontWeight = "bold";
element.style.border = "2px solid orange";
                    

Attributes & Data Attributes

Getting and Setting Attributes


const link = document.querySelector("a");

// Read attributes
console.log(link.getAttribute("href"));     // "https://example.com"
console.log(link.getAttribute("target"));   // "_blank" or null

// Set attributes
link.setAttribute("href", "https://new-url.com");
link.setAttribute("title", "Visit this link");

// Check if an attribute exists
console.log(link.hasAttribute("target"));   // true or false

// Remove an attribute
link.removeAttribute("title");
                

Common Properties vs. getAttribute

Many attributes have direct properties on the element. These are usually more convenient.


const img = document.querySelector("img");

// Direct properties (preferred for common attributes)
img.src = "new-image.jpg";
img.alt = "A beautiful sunset";
img.width = 400;

const link = document.querySelector("a");
link.href = "https://example.com";

const input = document.querySelector("input");
input.value = "Hello";
input.disabled = true;
input.placeholder = "Enter your name";
                

Data Attributes (data-*)

Custom data-* attributes let you store extra information on HTML elements. Access them through the dataset property.


<!-- In your HTML -->
<button data-action="delete" data-item-id="42" data-confirm="true">
    Delete Item
</button>
                

const btn = document.querySelector("button");

// Read data attributes via dataset
console.log(btn.dataset.action);    // "delete"
console.log(btn.dataset.itemId);    // "42" (camelCase!)
console.log(btn.dataset.confirm);   // "true" (always a string)

// Set data attributes
btn.dataset.status = "pending";
// Adds: data-status="pending" to the HTML

// Delete a data attribute
delete btn.dataset.confirm;
                

💡 data-* Naming Convention

HTML uses kebab-case (data-item-id), but JavaScript's dataset converts to camelCase (dataset.itemId). The conversion works both ways automatically.

graph TD A["Modifying Elements"] --> B["Text"] A --> C["Classes"] A --> D["Styles"] A --> E["Attributes"] B --> F["textContent
innerHTML"] C --> G["classList.add()
.remove()
.toggle()"] D --> H["style.property
getComputedStyle()"] E --> I["getAttribute()
setAttribute()
dataset"] style B fill:#ecfdf5,stroke:#22c55e,stroke-width:2px,color:#1e293b style C fill:#eff6ff,stroke:#3b82f6,stroke-width:2px,color:#1e293b style D fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,color:#1e293b style E fill:#fce7f3,stroke:#ec4899,stroke-width:2px,color:#1e293b

Hands-on Exercise

🏋️ Exercise: Dynamic Profile Card Editor

Objective: Use DOM selection and modification to build an interactive profile card. Create a new HTML file or use the console on any page.


<!-- Save this as profile.html and open in a browser -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Profile Card Editor</title>
    <style>
        body { font-family: sans-serif; padding: 2rem; background: #f1f5f9; }
        .profile-card {
            max-width: 400px; padding: 2rem; background: white;
            border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1);
        }
        .profile-card h2 { margin-top: 0; }
        .highlight { border: 3px solid #f59e0b; }
        .dark { background: #1e293b; color: #e2e8f0; }
        .hidden { display: none; }
    </style>
</head>
<body>
    <div class="profile-card" id="card"
         data-user-id="101" data-role="developer">
        <h2 id="name">Jane Doe</h2>
        <p id="bio">Full-stack developer who loves JavaScript.</p>
        <p id="location">📍 Austin, TX</p>
        <a id="website" href="https://example.com">Visit Website</a>
    </div>

    <script>
    // TODO: Complete each task using DOM methods

    // 1. Change the name to your name

    // 2. Update the bio text

    // 3. Change the location

    // 4. Update the website link (both text and href)

    // 5. Add the "highlight" class to the card

    // 6. Change the card's background color to a light blue

    // 7. Read and log the card's data-user-id

    // 8. Add a new data attribute: data-status="active"

    // 9. Toggle the "dark" class on and off the card

    // 10. Bonus: Create a function that takes a profile object
    //     and updates all fields at once
    //     updateProfile({ name: "Ray", bio: "...", location: "...", website: "..." })
    </script>
</body>
</html>
                    
💡 Hint

1-4: Use querySelector or getElementById to select, then set textContent for text and href for links.

5: Use classList.add().

6: Use style.backgroundColor.

7-8: Use dataset.userId to read and dataset.status = "active" to set.

9: Use classList.toggle("dark").

10: Write a function that takes an object parameter, destructures it, and sets each field.

✅ Solution

// 1. Change the name
const nameEl = document.querySelector("#name");
nameEl.textContent = "Ray De La Paz";

// 2. Update the bio
document.querySelector("#bio").textContent =
    "Technical trainer and course developer who loves building things.";

// 3. Change the location
document.querySelector("#location").textContent = "📍 Henderson, NV";

// 4. Update the website
const website = document.querySelector("#website");
website.textContent = "Visit Portfolio";
website.href = "https://rays-home.netlify.app/";

// 5. Add the highlight class
const card = document.querySelector("#card");
card.classList.add("highlight");

// 6. Change background color
card.style.backgroundColor = "#eff6ff";

// 7. Read data-user-id
console.log(card.dataset.userId);  // "101"

// 8. Add data-status
card.dataset.status = "active";
// Card now has: data-status="active"

// 9. Toggle dark mode
card.classList.toggle("dark");
// Call again to toggle off:
// card.classList.toggle("dark");

// 10. Bonus: Update all fields at once
function updateProfile({ name, bio, location, website }) {
    if (name) document.querySelector("#name").textContent = name;
    if (bio) document.querySelector("#bio").textContent = bio;
    if (location) document.querySelector("#location").textContent = `📍 ${location}`;
    if (website) {
        const el = document.querySelector("#website");
        el.href = website;
        el.textContent = "Visit Website";
    }
}

updateProfile({
    name: "Alice Smith",
    bio: "UX designer and coffee enthusiast.",
    location: "Portland, OR",
    website: "https://alice.dev"
});
                        

🎯 Quick Quiz

Question 1: What does querySelector return if no element matches?

Question 2: Why should you avoid innerHTML with user input?

Question 3: What method adds a CSS class without removing existing ones?

Question 4: How do you access data-item-id in JavaScript?

Summary

🎉 Key Takeaways

  • querySelector and querySelectorAll find elements using CSS selectors
  • textContent safely reads/writes plain text; innerHTML parses HTML (avoid with user data!)
  • classList methods (add, remove, toggle, contains) manage CSS classes precisely
  • style sets inline CSS using camelCase property names
  • Prefer toggling classes over setting inline styles for visual changes
  • dataset accesses data-* attributes with automatic kebab-to-camelCase conversion
  • getComputedStyle reads the actual rendered styles, not just inline ones
  • Convert NodeList to an array with [...nodeList] to use map/filter

📚 Additional Resources

🚀 What's Next?

You can now select and modify elements — but how do you respond when a user clicks a button, submits a form, or presses a key? The next lesson covers events and event listeners, the mechanism that makes web pages truly interactive.