Implement getElementsByTagName

Walk the tree depth-first from the root's children, collecting nodes whose tag name matches case-insensitively. Descendants only, never the root itself, and the depth-first order is what produces document order in the result.

Easy recursion tree traversal

The question

A tree traversal question dressed as a DOM question, which is why interviewers like it. Three details are being checked, and none of them is the recursion:

  1. Is matching case-insensitive? HTML tag names are.
  2. Is the root itself excluded? In the real DOM method it’s.
  3. Is the result in document order?

The solution

The walk starts at the children rather than the node, which is what excludes the root without needing a special case for it.

function getElementsByTagName(root, tagName) {
  const wanted = tagName.toLowerCase();
  const found = [];

  // Descendants only, and in document order, which is what the DOM method does.
  function walk(node) {
    for (const child of node.children ?? []) {
      if (child.tagName.toLowerCase() === wanted) found.push(child);
      walk(child);
    }
  }

  walk(root);
  return found;
}

How it works

The tests it has to pass

This is the part most explanations leave out. "It works" is not a claim you can check by reading. These are the 2 cases Practice Pad runs against your solution, and the implementation above passes all of them.

Easy2 cases · depth, case and order

finds matches at any depth regardless of letter case, in document order

const article = {
  tagName: "article",
  children: [
    {
      tagName: "section",
      children: [
        { tagName: "SPAN", children: [] },
        { tagName: "div", children: [{ tagName: "span", children: [] }] },
      ],
    },
    { tagName: "span", children: [] },
  ],
};
const spans = getElementsByTagName(article, "span");
expectEqual(spans.map((node) => node.tagName), ["SPAN", "span", "span"]);

returns every matching descendant, however deep

const tree = {
  tagName: "div",
  children: [
    { tagName: "p", children: [{ tagName: "p", children: [] }] },
    { tagName: "span", children: [] },
  ],
};
expectEqual(getElementsByTagName(tree, "p").length, 2);

Edge cases to watch out for

Including the root in the results

Calling getElementsByTagName("div") on a div must not return that div. Starting the walk at the children instead of the node removes the problem rather than special-casing it.

Comparing tag names case-sensitively

HTML tag names are case-insensitive and the DOM reports them uppercase, so a tree containing "SPAN" must match a search for "span". The first test mixes both cases deliberately.

Collecting in breadth-first order

A queue-based traversal returns all the shallow matches before the deep ones, which isn’t document order. Depth-first is what matches the real method.

Worth mentioning: the real getElementsByTagName returns a live HTMLCollection that updates as the DOM changes, unlike querySelectorAll which returns a static NodeList. Interviewers rarely ask you to implement liveness, but knowing the distinction is often the follow-up question.

Follow up questions

How would you extend this to handle a CSS selector?

Parse the selector into a matcher first, then reuse the same traversal with that matcher instead of the tag comparison. Class and id selectors are straightforward; combinators such as > and descendant matching are where it gets real, because you then need to evaluate against each node's ancestors rather than the node alone.

Why is tag matching case-insensitive?

HTML tag names are case-insensitive, and the DOM normalises them to uppercase in HTML documents, so element.tagName reads "DIV" even though you wrote <div>. Lowercasing both sides makes the comparison work whichever case the caller used.

What’s the difference between getElementsByTagName and querySelectorAll?

getElementsByTagName returns a live HTMLCollection that reflects later DOM changes; querySelectorAll returns a static NodeList snapshot. Liveness sounds convenient and is a classic source of bugs when you mutate the DOM while looping over the collection.

How would you implement this iteratively?

Use an explicit stack. Push the root's children, then repeatedly pop a node, test it and push its children. To preserve document order you push the children in reverse, since a stack reverses whatever you put on it.

Practise this with the tests running

Practice Pad runs these 2 cases against your own solution, in a real editor, on your Mac. Twenty-four questions, and an optional AI panel that explains why a test failed.

Download for macOS Free. Requires macOS 12 or later on Apple Silicon.

See all 24 questions