Skip to the content

Relational properties for parents, children and siblings

Last updated: By Lukas BrandtLesson 9 of 114 min read

querySelector finds elements by selector, starting from the top of the document, as you saw when accessing elements. Often, though, you already hold one element and need a neighbor: the list item around a button that was just clicked, the paragraph after a heading, the first entry in a list. Relational properties get you there by moving through the DOM tree from wherever you are.

A family tree

Take this checklist:

<ul class="checklist">
  <li class="task">
    <span class="task-name">Remove the old cooler</span>
    <button type="button" class="done-button">Done</button>
  </li>
  <li class="task">
    <span class="task-name">Clean the heat spreader</span>
    <button type="button" class="done-button">Done</button>
  </li>
  <li class="task">
    <span class="task-name">Apply fresh paste</span>
    <button type="button" class="done-button">Done</button>
  </li>
</ul>

The <ul> is the parent of the three <li> elements. They are its children, and they are siblings of each other. Each <li> is in turn the parent of a <span> and a <button>, which are siblings too. The DOM borrows the vocabulary of a family tree all the way down.

The properties

Property Starting from an element, it gives you When there is none
parentElement the element that contains it null
children its child elements, as an HTMLCollection an empty collection
firstElementChild its first child element null
lastElementChild its last child element null
nextElementSibling the next element at the same level null
previousElementSibling the previous element at the same level null
closest(selector) itself or the nearest ancestor matching the selector null
Parts links can earn us a commission. As an Amazon Associate I earn from qualifying purchases.

closest is a method rather than a property, but it does the same kind of work and you will probably use it more than all the others. Unlike the rest of the table, it checks the element itself before climbing, so button.closest('button') returns the button.

const list = document.querySelector('.checklist');

console.log(list.children.length);              // 3
console.log(list.firstElementChild.className);  // "task"

const second = list.children[1];
console.log(second.previousElementSibling === list.firstElementChild);  // true
console.log(list.lastElementChild.firstElementChild.textContent);       // "Apply fresh paste"
console.log(second.parentElement === list);                             // true

The lastElementChild.firstElementChild line chains two steps: from the list to its last <li>, then from that <li> to its first child, the <span>. Chains like this are fine for short hops. If you find yourself writing four in a row, querySelector on the nearest container is usually clearer.

Why "Element" is in the names

There is an older set of properties without that word: parentNode, childNodes, firstChild, nextSibling and so on. They work on nodes, and in the DOM the text between tags is a node too, including the line breaks and indentation in your HTML. So for the list above, list.firstChild is not the first <li> at all but a text node holding a newline and a few spaces. The Element versions skip text nodes and return only tags, which is nearly always what you want. parentNode and parentElement usually point to the same thing, since a text node can never be a parent.

Event delegation with closest

This is where relational properties earn their keep. Instead of one listener per button, the loop from the loops lesson, put a single listener on the list and work out what was clicked:

const list = document.querySelector('.checklist');

list.addEventListener('click', (event) => {
  const button = event.target.closest('.done-button');
  if (!button) {
    return;  // the click landed elsewhere in the list
  }

  const task = button.closest('.task');
  task.classList.toggle('is-done');
  button.textContent = task.classList.contains('is-done') ? 'Undo' : 'Done';
});

It works because clicks bubble, as the events lesson explained: a click on any button also reaches the <ul>. event.target is the element under the pointer, which might be the button or, in a fancier design, an icon inside it. closest('.done-button') climbs to the button in both cases and returns null when the click was on a task name, which the if handles by leaving early. A second closest then climbs from the button to its list item.

Delegation has two advantages over a listener on every button. It is one listener however long the list grows. And it keeps working for tasks you add later with createElement, because the listener sits on the parent, which was there all along.

Moving between siblings

Sibling properties suit jobs like moving a task one place up:

function moveUp(task) {
  const above = task.previousElementSibling;
  if (above) {
    above.before(task);
  }
}

before() inserts an element directly in front of another. An element can only be in one place, so task moves rather than being copied. For the first task, previousElementSibling is null, and the if leaves it where it is.

To take a task off the page completely, call task.remove(). Older code does the same thing with task.parentNode.removeChild(task), which you will still see in plenty of tutorials.

Try it

Paste the checklist and the delegation script into the online HTML editor, along with a style rule such as .task.is-done .task-name { text-decoration: line-through; }. Click a few buttons. Then add a second button with class="up-button" to each task and handle it inside the same listener: check event.target.closest('.up-button'), find its .task and pass it to moveUp.