In JavaScript, navigating “up” the DOM tree to find an element’s parent is a common task, especially when handling events or building dynamic components. There are two primary properties and one powerful method used for this purpose.
1. parentElement vs. parentNode
For the vast majority of web development tasks, these two properties are nearly identical, but they have a distinct technical boundary.
parentElement
This property returns the parent of the specified node only if it is an Element object (an HTML tag). If the parent is not an element (for example, the parent of the <html> tag is the document), it returns null.
const child = document.querySelector('.child');
const parent = child.parentElement;
if (parent) {
parent.style.border = '1px solid red';
}
parentNode
This is a more generic property from the older Node API. It returns any parent, including the Document node or DocumentFragment.
- The Key Difference: document.documentElement.parentNode returns the document object, while document.documentElement.parentElement returns null.
[!TIP] Which should I use? In 99% of cases, use parentElement. You are usually looking for a container to style or manipulate, and you don’t want your code to accidentally try to modify the document object.
2. closest(): Finding Specific Ancestors
One of the most powerful additions to modern JavaScript is the .closest() method. Instead of manually climbing the tree with parentElement.parentElement, you can look for the first ancestor that matches a specific CSS selector.
Syntax
const ancestor = element.closest(selector);
Why it’s useful:
- Event Delegation: If a user clicks a button inside a complex card, you can easily find the card element.
- Self-Inclusion: The method starts by checking the element itself. If the element matches the selector, it returns the element. If not, it moves to the parent, and so on.
document.addEventListener('click', (event) => {
// Find the nearest list item from the click target
const listItem = event.target.closest('li');
if (listItem) {
console.log('You clicked inside list item:', listItem.id);
}
});
3. Traversing Multiple Levels
If you must traverse without a selector, you can chain the properties, though this is generally discouraged for maintainability.
// Moves up two levels
const grandParent = child.parentElement.parentElement;
[!TIP] Always check for existence when chaining:
child.parentElement?.parentElement. If the first parent is null, the script will throw an error without the optional chaining?.operator.
4. Technical Nuance: Shadow DOM
When working with Web Components and the Shadow DOM, parentElement will return null if you reach the shadow root. To jump from a shadow root back to the “light DOM” host, you must use the assignedSlot property or navigate via the host property of the shadow root.
JavaScript
// Inside a Shadow DOM context
const hostElement = myShadowElement.getRootNode().host;
5. Practical Use Case: Event Delegation
Getting the parent is essential when you want to handle a click on a button but perform an action on the entire row or container that holds that button.
document.addEventListener('click', (event) => {
if (event.target.classList.contains('delete-btn')) {
// Find the list item containing this button and remove it
const listItem = event.target.closest('li');
listItem.remove();
}
});
