DOM traversal
Once you have an element, you often need the ones around it: its children, its siblings, elements inside a shadow root, or content inside an iframe. This guide covers moving through the DOM tree from a known starting point. To locate that starting element in the first place, see Element finding.
Get child elements
get_children_elements() returns the descendants of an element. max_depth controls how deep it goes (1 is direct children only), and tag_filter keeps only the tags you name.
import asyncio
from pydoll.browser.chromium import Chrome
async def main():
async with Chrome() as browser:
tab = await browser.start()
await tab.go_to('https://books.toscrape.com')
container = await tab.find(class_name='row', tag_name='ol')
direct = await container.get_children_elements(max_depth=1)
print(f'{len(direct)} direct children')
# descendants up to 2 levels deep, links only
links = await container.get_children_elements(max_depth=2, tag_filter=['a'])
print(f'{len(links)} links within two levels')
asyncio.run(main())
Get sibling elements
get_siblings_elements() returns the elements at the same level as your element, excluding it. tag_filter narrows the result to specific tags.
active = await tab.find(class_name='active')
siblings = await active.get_siblings_elements()
print(f'{len(siblings)} siblings')
link_siblings = await active.get_siblings_elements(tag_filter=['a'])
Scoped search vs direct children
A scoped find() or query() searches all descendants of an element. When you want only the direct children, use the CSS child combinator > or an XPath step, which query() accepts:
container = await tab.find(id='cards')
# every .card anywhere in the subtree
all_cards = await container.find(class_name='card', find_all=True)
# only the .card elements that are direct children
direct_cards = await container.query('> .card', find_all=True)
Use get_children_elements() when you want to explore structure or filter by tag; use a scoped find()/query() when you want elements matching specific attributes anywhere in the subtree.
Read text and attributes
From any element you can read its visible text and its HTML attributes:
book = await tab.find(class_name='product_pod')
title = await book.find(tag_name='h3')
print(await title.text) # visible text
link = await title.find(tag_name='a')
print(link.get_attribute('href')) # an attribute value
print(link.get_attribute('title'))
text is an awaitable property; get_attribute(name) returns the attribute string, or None when the attribute is absent.
Shadow DOM
Many components hide their internals inside a shadow root, which regular DOM queries can't see. Access the shadow host, get its shadow root, then search inside it.
host = await tab.find(id='my-component')
shadow = await host.get_shadow_root()
button = await shadow.query('.internal-btn')
await button.click()
Inside a shadow root, use query() with CSS
find() and XPath are not supported on a ShadowRoot and raise NotImplementedError. Search shadow roots with query() and CSS selectors only.
query() inside a shadow root takes the usual find_all, timeout, and raise_exc parameters:
items = await shadow.query('.item', find_all=True)
dynamic = await shadow.query('#late', timeout=5, raise_exc=False)
Web components nest, so a shadow root can contain another shadow host:
outer = await tab.find(tag_name='outer-component')
outer_shadow = await outer.get_shadow_root()
inner = await outer_shadow.query('inner-component')
inner_shadow = await inner.get_shadow_root()
deep = await inner_shadow.query('.deep-btn')
Discover shadow roots on a page
When you don't know which shadow roots exist (debugging, or dynamic widgets like Cloudflare Turnstile), find_shadow_roots() returns all of them. Shadow hosts often load late, so pass timeout to poll until they appear:
shadow_roots = await tab.find_shadow_roots(timeout=10)
for sr in shadow_roots:
print(f'mode={sr.mode}, host={sr.host_element}')
checkbox = await sr.query('input[type="checkbox"]', raise_exc=False)
if checkbox:
await checkbox.click()
By default the search covers the main document (including same-origin iframes). Pass deep=True to also reach shadow roots inside cross-origin iframes (OOPIFs), which is what widgets like Turnstile use:
Work inside an iframe
An iframe has its own DOM context. Find the iframe element, then call find() or query() on it; Pydoll routes the search into the frame automatically. Keep chaining for nested iframes.
iframe = await tab.query('iframe.embedded-content', timeout=10)
button = await iframe.find(tag_name='button', class_name='submit')
await button.click()
# nested iframe
inner = await iframe.find(tag_name='iframe')
link = await inner.find(text='Download PDF')
await link.click()
For a full iframe guide including CAPTCHA frames and troubleshooting, see Iframes.
Screenshots inside iframes
tab.take_screenshot() captures the top-level page only. To capture iframe content, find an element inside the frame and call element.take_screenshot().
What's next
- Element finding: locate the elements you traverse from.
- Iframes: the complete guide to frame contexts.
- Structured extraction: let a model walk repeating structures for you.