Node properties: type, tag and contents

You already know the DOM is a tree of nodes. Now look closer at the nodes themselves: what kind of objects they are, and which properties you reach for every day to read a node’s type, its tag, and the content inside it.

DOM node classes

Not every node carries the same properties. The object standing in for an <a> tag has link properties like href. The object for an <input> carries input properties like value and type. A text node has neither — it is a different kind of thing entirely. Yet all of them share a common floor of properties and methods, because every DOM node class descends from one hierarchy.

Each node is an instance of a specific built-in class. The root of the family is EventTarget. Node inherits from it, and everything else branches off from there.

EventTargetNodeDocumentCharacterDataElementTextCommentHTMLElementSVGElement, …HTMLInputElementHTMLBodyElementHTMLAnchorElement…and so on
The DOM class hierarchy. Arrows point from a class to the parent it inherits from. Abstract classes (never instantiated directly) are shown in muted text.

Here is what each class in that chain does:

  • EventTarget is the abstract root. No object is ever created directly from it. Its whole job is to give every node the ability to receive events, which you’ll study later.

  • Node is also abstract, never instantiated on its own. It supplies the core tree navigation — parentNode, nextSibling, childNodes, and friends (all getters). The concrete node classes inherit these.

  • Document represents the document as a whole. For historical reasons browsers often expose it through HTMLDocument, though the current spec no longer requires that layer. The global document object is an instance of this class and serves as your entry point into the DOM.

  • CharacterData is another abstract class, split into two concrete ones:

    • Text — the text inside an element, such as Hello in <p>Hello</p>.
    • Comment — HTML comments. They’re invisible on the page, but each one still becomes a node in the DOM.
  • Element is the base class for elements. It adds element-only navigation like nextElementSibling and children, plus search methods like getElementsByTagName and querySelector. Browsers render more than HTML — XML and SVG too — so Element branches into SVGElement, XMLElement (not our concern here), and HTMLElement.

  • HTMLElement is the base for every HTML element, and where you’ll spend most of your time. It’s inherited by the concrete per-tag classes:

Many tags get their own class with extra properties, but not all. Plain containers like <span>, <section>, and <article> add nothing special, so they’re plain HTMLElement instances.

The set of properties a node exposes is the sum of everything along its inheritance chain. Take the object for an <input>. It belongs to HTMLInputElement, and it stacks up like this:

HTMLInputElement
value, type, checked…
HTMLElement
hidden, innerText, dataset…
Element
innerHTML, querySelector…
Node
parentNode, childNodes…
EventTarget
addEventListener…
Object
hasOwnProperty, toString…
An <input> element's DOM object gathers properties by layering every class in its inheritance chain, from input-specific at the top down to plain Object at the bottom.

Reading top to bottom: HTMLInputElement adds input-specific properties, HTMLElement adds common HTML element behavior, Element adds generic element methods, Node adds tree properties, EventTarget adds event support, and finally everything inherits from plain Object, so hasOwnProperty and the rest are there too.

Want to see a node’s class name at runtime? Every object has a constructor property pointing at its class, and constructor.name is the name string:

alert( document.body.constructor.name ); // HTMLBodyElement

Or just coerce it to a string:

alert( document.body ); // [object HTMLBodyElement]

You can also test the chain with instanceof. Because document.body sits at the bottom of a long inheritance line, it answers true to every class above it:

alert( document.body instanceof HTMLBodyElement ); // true
alert( document.body instanceof HTMLElement ); // true
alert( document.body instanceof Element ); // true
alert( document.body instanceof Node ); // true
alert( document.body instanceof EventTarget ); // true

None of this is a special DOM-only mechanism. DOM nodes are ordinary JavaScript objects using ordinary prototype-based inheritance. Log one with console.dir(elem) in a browser and you can walk down HTMLElement.prototype, Element.prototype, and so on in the console.

The “nodeType” property

nodeType is the old-school way to ask what kind of node you’re holding. It predates the class-based checks and returns a number:

  • elem.nodeType == 1 for element nodes,
  • elem.nodeType == 3 for text nodes,
  • elem.nodeType == 9 for the document object,
  • plus a handful of other values listed in the specification.
1
element
<body>, <div>…
3
text
“Hello”
9
document
document
The three nodeType values you'll meet most: 1 for elements, 3 for text, 9 for the document.

An example that inspects three nodes in one document:

<body>
  <script>
  let elem = document.body;

  // what type of node is in elem?
  alert(elem.nodeType); // 1 => element

  // and its first child is...
  alert(elem.firstChild.nodeType); // 3 => text

  // the document object itself reports 9
  alert( document.nodeType ); // 9
  </script>
</body>

In modern code the class-based tests (instanceof, or checking tagName) are usually clearer, but nodeType can be the shorter tool when you just need “is this an element or a text node?”. Note it’s read-only — you can’t reassign a node’s type.

Tag: nodeName and tagName

Given a node, two properties report its tag name: nodeName and tagName.

alert( document.body.nodeName ); // BODY
alert( document.body.tagName ); // BODY

So why two? The difference is small but real, and it comes down to which class defines each:

  • tagName exists only on Element nodes — it comes from the Element class.
  • nodeName is defined on every Node:
    • for elements it returns the same string as tagName,
    • for other node types (text, comment, document) it returns a label describing what the node is.
nodetagNamenodeName
<body>BODYBODY
commentundefined#comment
documentundefined#document
tagName only answers for elements. nodeName answers for every node type, returning a #-prefixed label for non-elements.

Here’s that table proven in code, comparing a comment node and the document:

<body><!-- comment -->

  <script>
    // for the comment node
    alert( document.body.firstChild.tagName ); // undefined (not an element)
    alert( document.body.firstChild.nodeName ); // #comment

    // for the document
    alert( document.tagName ); // undefined (not an element)
    alert( document.nodeName ); // #document
  </script>
</body>

If your code only ever touches elements, the two are interchangeable — reach for whichever reads better.

Pick a node below and watch all three properties at once. Notice how tagName reports undefined for the comment and the document, while nodeName always has an answer:

interactiveInspecting nodeType, nodeName and tagName

innerHTML: the contents

The innerHTML property gives you the HTML inside an element, as a string. Read it, or assign to it — writing to innerHTML is one of the most direct ways to rewrite part of the page.

This example reads the body’s current markup, then blows it away and replaces it:

<body>
  <p>A paragraph</p>
  <div>A div</div>

  <script>
    alert( document.body.innerHTML ); // read the current contents
    document.body.innerHTML = 'The new BODY!'; // replace it
  </script>

</body>

Try it live. Edit the markup, set it on the target, and see the read-back of innerHTML afterwards — note that the browser may normalize what you typed:

interactiveReading and writing innerHTML

Feed it broken markup and the browser cleans up after you. A missing closing tag gets closed automatically:

<body>

  <script>
    document.body.innerHTML = '<b>test'; // forgot to close the tag
    alert( document.body.innerHTML ); // <b>test</b> (fixed)
  </script>

</body>

Beware: “innerHTML+=” does a full overwrite

You can seemingly append markup with elem.innerHTML += "more html":

feedDiv.innerHTML += "<div>New reply<img src='avatar.png'/> !</div>";
feedDiv.innerHTML += "Tap to open";

Be careful. This is not an append — it’s a complete rebuild. The += operator expands to a read followed by a full assignment:

elem.innerHTML += "...";
// is shorthand for:
elem.innerHTML = elem.innerHTML + "..."

So each += runs two steps:

old nodes
in the DOM
1. read as string
“old” + “new”
2. wipe & reparse
brand-new nodes
resources reloaded
innerHTML += doesn't add to the existing DOM. It serializes the old content to a string, concatenates, then discards the old nodes and parses the whole thing from scratch.
  1. The old contents are removed.
  2. Fresh HTML — the old string concatenated with the new — is parsed and written in its place.

Because the content is zeroed out and rebuilt from a string, every image and other resource inside gets reloaded.

In the feedDiv example, feedDiv.innerHTML += "Tap to open" re-creates the whole subtree and re-fetches avatar.png (hopefully it’s cached, which depends on the response’s HTTP caching headers). With a lot of existing text and images, that reload flicker becomes obvious.

There are quieter side effects too. If the user had text selected inside the element, most browsers drop the selection when innerHTML is rewritten. If there was an <input> with typed-in text, that text is gone. And so on down the list.

Better tools exist for genuine appending, and you’ll meet them shortly in the chapter on modifying the document.

outerHTML: full HTML of the element

outerHTML is innerHTML plus the element’s own tag — the complete HTML of the element, wrapper included.

<div id="elem">Draft <b>copy</b></div>

<script>
  alert(elem.outerHTML); // <div id="elem">Draft <b>copy</b></div>
</script>

Reading it is unremarkable. Writing to it has a surprise: assigning to outerHTML does not modify the element. It replaces the element in the DOM and leaves your original object untouched.

That’s odd enough to earn its own warning. Look closely:

<div>Draft copy</div>

<script>
  let div = document.querySelector('div');

  // replace div.outerHTML with <p>...</p>
  div.outerHTML = '<p>Final copy</p>'; // (*)

  // 'div' is still the same!
  alert(div.outerHTML); // <div>Draft copy</div> (**)
</script>

On line (*) the page swaps: where the <div> was, the DOM now shows <p>Final copy</p>. But line (**) still reports the old <div> markup. The div variable never changed.

Here’s the mental model. The assignment doesn’t reach into the object div points at and edit it. Instead it detaches that object from the document and drops the newly parsed HTML into the gap:

the DOM (document)
<p>Final copy</p>
variable div
<div>Draft copy</div>
(detached, unchanged)
After div.outerHTML = '...', the DOM holds the new <p>, but the div variable still references the detached original <div> object — nothing wrote the new node back to any variable.

Step by step, div.outerHTML = '<p>Final copy</p>' did this:

  • div was removed from the document.
  • The parsed <p>A new element</p> took its place.
  • The div variable kept its old value. The new element wasn’t saved to any variable.

The trap is easy to fall into: overwrite elem.outerHTML, then keep working with elem as though it now holds the new content. It doesn’t. That intuition is right for innerHTML but wrong for outerHTML. If you need a reference to the replacement, query the DOM for it after the assignment.

nodeValue/data: text node content

innerHTML is an Element-only property. Text and comment nodes aren’t elements, so they don’t have it. Their equivalent is a pair of properties: nodeValue and data. For everyday use the two are interchangeable — the spec draws only minor distinctions between them — so we’ll use data because it’s shorter.

Reading the content of a text node and a comment node:

<body>
  Hello
  <script>
    let text = document.body.firstChild;
    alert(text.data); // Hello

    let comment = text.nextSibling;
    alert(comment.data); // Comment
  </script>
</body>

Reading a text node’s content makes obvious sense. But why would you read a comment? Because comments can carry hidden instructions. Some tools embed template directives inside them:

  <div>Book now<!-- track: cta-top --></div>

JavaScript can pull those out through data and act on the embedded instructions.

textContent: pure text

textContent gives you the text inside an element with every tag stripped out — text only, no markup.

<div id="recipe">
  <h1>Lemon Cake</h1>
  <p>Bake for 40 minutes.</p>
</div>

<script>
  // Lemon Cake Bake for 40 minutes.
  alert(recipe.textContent);
</script>

The tags are gone; the text that lived inside them remains, concatenated.

Reading textContent is only occasionally useful. Writing to it is where the property earns its keep, because it inserts text the safe way.

Say you have an arbitrary string — something a user typed — and you want to display it. Your choice of property decides how the browser treats it:

  • Assign it to innerHTML and the string is parsed as HTML. Any tags in it become real elements.
  • Assign it to textContent and the string is treated as literal text. Every character shows exactly as written.
elem.innerHTML = name
Maya
tags become real markup
elem.textContent = name
<b>Maya</b>
shown literally as text
The same user string, two properties. innerHTML parses <b> into markup; textContent shows the angle brackets literally as text.

The code behind that figure:

<div id="elem1"></div>
<div id="elem2"></div>

<script>
  let name = prompt("What's your name?", "<b>Maya</b>");

  elem1.innerHTML = name;
  elem2.textContent = name;
</script>
  1. The first <div> receives the name as HTML: the <b> tag renders, so you see a bold name.
  2. The second <div> receives it as text, so you literally see <b>Maya</b> on the page.

Type anything into the field below — include some tags — and compare the two panels. The innerHTML panel parses your text as markup; the textContent panel shows every character exactly as written:

interactiveinnerHTML vs textContent with user input

Most of the time, data coming from a user should stay text — you don’t want their input injecting HTML into your page. Assigning to textContent does exactly that, and it’s a first line of defense against injection.

The “hidden” property

The hidden attribute — and its matching DOM property — controls whether an element is shown. Set it in HTML or from JavaScript:

<div>Both divs below are hidden</div>

<div hidden>With the attribute "hidden"</div>

<div id="elem">JavaScript assigned the property "hidden"</div>

<script>
  elem.hidden = true;
</script>

Functionally hidden matches style="display:none", just shorter to write and clearer in intent.

A tiny demo: toggle the property by hand, or flip it on a timer to make the element blink. Watch the live read-out of box.hidden as it changes:

interactiveToggling the hidden property

More properties

Beyond the shared set, elements carry properties that depend on their specific class:

  • value — the current value of an <input>, <select>, or <textarea> (HTMLInputElement, HTMLSelectElement, and so on).
  • href — the destination of <a href="..."> (HTMLAnchorElement).
  • id — the id attribute, available on every element (HTMLElement).
  • …and many more.

For example:

<input type="text" id="elem" value="value">

<script>
  alert(elem.type); // "text"
  alert(elem.id); // "elem"
  alert(elem.value); // value
</script>

Most standard HTML attributes have a matching DOM property you can read and write like this.

To find every property a class supports, the specification is the authority — HTMLInputElement, for instance, is documented at https://html.spec.whatwg.org/#htmlinputelement. For a quick look, or to see what a specific browser exposes, log the element with console.dir(elem) and browse, or open the “Properties” pane in the Elements tab of your dev tools.

Summary

Every DOM node is an instance of a class, and those classes form an inheritance hierarchy. A node’s full set of properties and methods is the accumulation of that entire chain.

The core node properties:

nodeType

Tells you whether a node is an element, text, or something else, as a number: 1 for elements, 3 for text nodes, 9 for the document, plus a few others. Read-only.

nodeName/tagName

The tag name for elements (uppercase outside XML mode). For non-element nodes, nodeName returns a label describing the node while tagName is undefined. Read-only.

innerHTML

The HTML markup inside an element, as a string. Readable and writable.

outerHTML

The full HTML of the element, its own tag included. Writing to elem.outerHTML leaves elem untouched — it replaces the element in the document instead.

nodeValue/data

The content of a non-element node such as text or a comment. The two are near-identical; data is the shorter one to reach for. Writable.

textContent

The text inside an element with all tags removed. Writing to it inserts a string as literal text — special characters and tag syntax are shown verbatim, never parsed — which is the safe way to display user-supplied content.

hidden

Set it to true to hide the element, equivalent to CSS display:none.

Nodes also carry class-specific properties: <input> (HTMLInputElement) has value and type, <a> (HTMLAnchorElement) has href, and so on. Most standard HTML attributes have a corresponding DOM property.

But attributes and properties are not always the same thing, and the difference between them is the subject of the next chapter.