HAP looking confused while studying a map

Station 3: Reading the Room

How to read text, HTML, and attributes from DOM elements

Station 2 gave me querySelector. I was confident. I knew it was the modern way to find elements. I knew it used CSS selectors. I was ready.

So I typed this: document.querySelector('robot-name') — and got null. Nothing. The element was RIGHT THERE on the page, but querySelector couldn't find it.

Then it hit me — I'd used querySelector without actually knowing how to write a CSS selector. I had the tool but I didn't speak its language.

This time, I didn't push back. I asked for help. 🟠

🔬 Try it yourself: Selector Playground →

Quick Reference

CSS Selectors and DOM Reading Methods →

I Had the Tool but Not the Language

Here's what I typed, full of confidence:

My first attempt:
const name = document.querySelector('robot-name');
console.log(name);  // null — nothing found!

No dot, no hash — I was searching for a <robot-name> HTML tag that doesn't exist.

I stared at the screen. The element was right there in my HTML — <span id="robot-name"> — but querySelector returned null. I had the right tool from Station 2, but I didn't know how to tell it what to look for.

This time, I didn't push back. I asked for help.

Leaning on AI — The Right Call This Time

I described the error to an AI assistant: "I'm using querySelector with the element's ID but getting null." The response was clear:

Bare Word = Tag Name

'div' or 'span' — searches for an HTML element by its tag name. That's what 'robot-name' was doing — looking for a <robot-name> tag.

Dot = Class

'.card-header' — the dot prefix means "find an element with this class." Same dot from CSS stylesheets.

Hash = ID

'#robot-name' — the hash prefix means "find the element with this ID." Same hash from CSS stylesheets.

Brackets = Attribute

'[data-status]' — square brackets match elements by their attributes. Same syntax from CSS.

🟠 The Lightbulb Moment:

Wait — the dot and hash from my CSS stylesheets? That's the SAME syntax? querySelector uses CSS selector syntax because it IS CSS selector syntax. Everything I'd learned writing stylesheets transferred directly.

In Station 2, I pushed back when I thought I was being steered wrong. That was the right call there. But this time, I had a knowledge gap, not a disagreement. Pushing back isn't the default — it's the response to a specific signal.

The Selector Toolkit

Here are the selectors I practiced on the Robot ID Card:

CSS selectors in JavaScript:
// By ID — finds the one element with this ID
const name = document.querySelector('#robot-name');

// By class — finds the FIRST element with this class
const header = document.querySelector('.card-header');

// By tag — finds the FIRST element of this type
const heading = document.querySelector('h2');

// Descendant selector — class + tag combined
const headerTitle = document.querySelector('.card-header h2');

// By attribute — finds elements with this attribute
const statusEl = document.querySelector('[data-status]');
querySelectorAll — when there are multiple matches:
// Returns a NodeList of ALL matching elements, not just the first
const allRows = document.querySelectorAll('.info-row');
console.log(allRows.length);  // 3 — every element with class "info-row"

querySelector returns one element (or null). querySelectorAll returns a collection of all matches.

🟠 If this feels like a lot of selectors to absorb at once, that's okay — it was a lot for me too. The Selector Playground demo lets you try each one live on the Robot ID Card. I found that more helpful than memorizing the syntax.

Now That I Found It... What Can I Read?

Finding an element was only half the challenge. Once I had it, I needed to read what was inside. Here's the HTML structure of the card header I was working with:

The card-header HTML:
<div class="card-header">
    <span class="robot-emoji">🤖</span>
    <h2>Robot <span class="highlight">ID Card</span></h2>
</div>

I assumed there'd be one way to read the contents. There are three — and they each return something different.

Three Ways to Read — Three Different Answers

I ran all three reading methods on the same .card-header element. The results surprised me:

Reading the card-header three ways:
const header = document.querySelector('.card-header');

// textContent — ALL text, including hidden elements
console.log(header.textContent);
// "🤖\n    Robot ID Card"

// innerHTML — the raw HTML markup inside
console.log(header.innerHTML);
// '<span class="robot-emoji">🤖</span>\n    <h2>Robot <span class="highlight">ID Card</span></h2>'

// innerText — only VISIBLE text, layout-aware
console.log(header.innerText);
// "🤖\nRobot ID Card"

textContent

Returns all text content, including text inside hidden elements. Fast. Does not trigger a page reflow. This is the safe default for reading text.

innerText

Returns only visible text — it's aware of CSS styling. If an element is hidden with display: none, innerText skips it. Slower because it checks layout.

innerHTML

Returns the raw HTML markup as a string — tags and all. Powerful for reading structure, but dangerous for writing (more on that in a moment).

Grace Drew a Line

I mentioned to Grace Hopper that textContent and innerText seemed like the same thing. She corrected me immediately.

Grace Hopper: "They are not the same. textContent returns all text content of a node and its descendants, including text within hidden elements. innerText is aware of styling and returns only the text that is visually rendered. The distinction matters when elements are hidden with CSS."

She was right, of course. If I had a <span style="display: none">secret</span> inside an element, textContent would include "secret" but innerText would not. For most cases, textContent is the right choice — it's faster and more predictable.

innerHTML — Powerful and Dangerous

When I discovered innerHTML, I got excited. I could read the entire HTML structure of any element as a string! I started imagining all the ways I could use it to build dynamic pages.

Grace shut that down fast.

Grace Hopper: "innerHTML is powerful. That is precisely why it is dangerous. When you use innerHTML to write content, the browser parses any HTML in the string — including script tags and event handlers. The original designers of the DOM did not anticipate how it would be exploited. Do not repeat their oversight."

🟠 What I Took Away:

innerHTML is fine for reading — when I need to see the HTML structure inside an element. But using it for writing content opens the door to security vulnerabilities. Grace's warning planted a seed — I'll learn more about this at Station 4 when I start changing things.

🟠 Three reading methods is a lot to keep straight. The cheat sheet has all three side by side for quick reference — I still check it when I forget which one to use.

Reading Attributes

Text content isn't the only thing worth reading. Elements also have attributes — the extra information tucked inside their opening tags.

getAttribute() examples:
const statusEl = document.querySelector('#status-message');

// Read the data-status attribute
console.log(statusEl.getAttribute('data-status'));  // "online"

// Read the id attribute
console.log(statusEl.getAttribute('id'));  // "status-message"

// Read a class attribute — returns the FULL string
const header = document.querySelector('.card-header');
console.log(header.getAttribute('class'));  // "card-header"
// NOT an array! Just one big string, even with multiple classes

🟠 Gotcha I Discovered:

getAttribute('class') returns the entire class string — like "card-header active highlighted". It doesn't return an array. If an element has multiple classes, they all come back as one space-separated string. I spent twenty minutes trying to figure out why my comparison wasn't working before I realized this.

What You'll Learn at This Station

HAP's Discovery: Selectors are the language of querySelector, and once I found an element, I had three different ways to read it — each returning something different. Here are the three key insights I walked away with:

🎯 CSS Selectors Are the Key

#id .class tag

querySelector uses the same selector syntax from CSS stylesheets — dots for classes, hashes for IDs, bare words for tags.

📖 Three Reading Methods

textContent

textContent, innerHTML, and innerText all read from elements — but each returns something different. textContent is the safe default.

🔍 Attributes Hold Data

getAttribute()

Elements carry extra information in attributes. getAttribute() reads them, but watch out — class comes back as a string, not an array.

HAP surrounded by tangled code with an 'oops' expression

HAP's Confession:

  • I wrote querySelector('robot-name') without the hash and got null. Spent ten minutes checking if the element existed before realizing I was searching for an HTML tag called <robot-name>.
  • I used innerHTML when I only needed the text. Got back a string full of <span> tags and had to parse it. textContent would have given me exactly what I needed.
  • I thought textContent and innerText were identical until a hidden element tripped me up. Grace's correction saved me from a subtle bug.
  • I assumed getAttribute('class') would return an array of class names. Nope — one long string. My === 'active' comparison failed silently on an element with multiple classes.

🟠 Try It Yourself:

I built a Selector Playground where I can type any CSS selector and see what querySelector finds — plus the textContent, innerHTML, and innerText side by side. It made all of this click for me.

🎓 Selectors and Reading Quick Reference

1

CSS Selectors in JS

querySelector uses CSS selector syntax: #id for IDs, .class for classes, tag for elements, [attr] for attributes. The same syntax from stylesheets.

2

querySelector vs querySelectorAll

querySelector returns the first match (or null). querySelectorAll returns a NodeList of every match. Use the right one for the job.

3

textContent — The Safe Default

Returns all text, including hidden content. Fast and predictable. When in doubt, use textContent for reading text from elements.

4

innerHTML — Powerful but Dangerous

Returns HTML markup as a string. Great for reading structure, but writing with it opens security risks. Grace says: "Do not repeat their oversight."

5

innerText — Visibility-Aware

Returns only visible text. Skips hidden elements. Slower than textContent because it checks CSS layout. Use when visibility matters.

6

getAttribute() for Attributes

Reads any attribute: getAttribute('data-status'), getAttribute('id'). Remember that getAttribute('class') returns a string, not an array.

What's Next?

So far, I've learned to find elements (Station 2) and read what's inside them (this station). But reading is watching from the sidelines. What happens when I start CHANGING things?

At Station 4, I'll learn to modify text, update attributes, and change styles — turning my read-only relationship with the DOM into a two-way conversation. And I'll find out exactly why Grace warned me about innerHTML. 🟠

Quick Reference

CSS Selectors and DOM Reading Methods →