Skip to content

useBetterDomTraversing (JavaScript)

Language JavaScript (and super languages)
biome.json
{
"linter": {
"rules": {
"nursery": {
"useBetterDomTraversing": "error"
}
}
}
}

Prefer modern DOM traversal APIs over positional indexes and chained walks.

Named first-child accessors, querySelector(), and closest() describe intent more clearly than childNodes[0], children[n], and repeated .parentElement access. Merging chained .querySelector() calls with static selectors has the same benefit.

Fixes are unsafe because the replacement is not always equivalent:

  • .childNodes[0] is undefined when empty; .firstChild is null
  • .closest() looks for any matching ancestor, not an exact number of .parentElement hops
  • chained .querySelector() calls search inside the first match, while a combined selector searches from the original node

props.children is ignored because that is component data, not DOM traversal.

element.childNodes[0];
code-block.js:1:1 lint/nursery/useBetterDomTraversing  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

This indexes .childNodes to get the first child.

> 1 │ element.childNodes[0];
^^^^^^^^^^^^^^^^^^^^^
2 │

A positional index is harder to read than a named first-child accessor.

This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.

Unsafe fix: Use .firstChild instead.

1 - element.childNodes[0];
1+ element.firstChild;
2 2

element.children[0];
code-block.js:1:1 lint/nursery/useBetterDomTraversing  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

This indexes .children to get the first element child.

> 1 │ element.children[0];
^^^^^^^^^^^^^^^^^^^
2 │

A positional index is harder to read than a named first-child accessor.

This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.

Unsafe fix: Use .firstElementChild instead.

1 - element.children[0];
1+ element.firstElementChild;
2 2

element.children[2];
code-block.js:1:1 lint/nursery/useBetterDomTraversing ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

This uses a positional index on .children.

> 1 │ element.children[2];
^^^^^^^^^^^^^^^^^^^
2 │

A CSS selector describes which child you want without relying on a numeric index.

Replace this with .querySelector() and a selector for the child.

This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.

element.parentElement.parentElement;
code-block.js:1:1 lint/nursery/useBetterDomTraversing ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

This walks ancestors by chaining .parentElement.

> 1 │ element.parentElement.parentElement;
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2 │

.closest() looks up a matching ancestor without repeating .parentElement.

Replace this chain with .closest() and a selector for the ancestor.

This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.

element.querySelector("a").querySelector("b");
code-block.js:1:1 lint/nursery/useBetterDomTraversing  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

These .querySelector() calls are chained.

> 1 │ element.querySelector("a").querySelector("b");
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2 │

One .querySelector() call with a combined selector is easier to read.

This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.

Unsafe fix: Merge the .querySelector() calls.

1 - element.querySelector("a").querySelector("b");
1+ element.querySelector(":scope·a·b");
2 2

element.firstChild;
element.firstElementChild;
element.querySelector("li");
element.closest("form");
const child = props.children[0];