I Had the Tool but Not the Language
Here's what I typed, full of confidence:
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:
// 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]'); // 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:
<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:
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.
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'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
innerHTMLwhen I only needed the text. Got back a string full of<span>tags and had to parse it.textContentwould have given me exactly what I needed. - I thought
textContentandinnerTextwere 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
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.
querySelector vs querySelectorAll
querySelector returns the first match (or null). querySelectorAll returns a NodeList of every match. Use the right one for the job.
textContent — The Safe Default
Returns all text, including hidden content. Fast and predictable. When in doubt, use textContent for reading text from elements.
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."
innerText — Visibility-Aware
Returns only visible text. Skips hidden elements. Slower than textContent because it checks CSS layout. Use when visibility matters.
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. 🟠