DocHub
Complete DOM selector reference for extracting data from WhatsApp Web — chat list, messages, contacts, media

WhatsApp Web DOM Structure Reference

Last verified: 2026-02-19 against WhatsApp Web running in headless Chromium via Puppeteer.

This document maps how to extract data from WhatsApp Web’s rendered HTML DOM. It also lists the forbidden Store API equivalents (for documentation only — those methods are NOT to be used).


Page Layout

WhatsApp Web requires a viewport of at least ~1000px wide to render the two-column desktop layout. Below that, it shows a single-column mobile view where clicking a chat replaces the chat list.

Recommended viewport: 1366 x 768 (set via Puppeteer defaultViewport and --window-size)

Top-Level Structure

#app
  └─ div (flex, full-size)
       └─ div (block, 11 children)
            ├─ #wa-popovers-bucket (empty)
            ├─ #expressions-panel-container (emoji picker, usually empty)
            ├─ div (flex container for main content — 2 columns)
            │    └─ div (8 children — the actual panels)
            │         ├─ div (full-width background layer)
            │         ├─ HEADER data-tab="2" (64x768 — left nav bar: Chats/Status/Channels/Communities)
            │         ├─ div (overlay layer)
            │         ├─ div (background layer)
            │         ├─ div (410x768 — CHAT LIST panel, contains #pane-side)
            │         ├─ div (891x768 — CONVERSATION panel, contains #main when chat open)
            │         └─ div (1px separator)
            └─ ...

Key Panel IDs

Panel Selector Visibility
Chat list #pane-side Always visible
Conversation #main Only when a chat is open
Contact info [data-animate-drawer-right="true"] Only when contact info panel is open

Chat List (#pane-side)

Container Structure

#pane-side
  ├─ BUTTON (aria-label="Chats" — tab selector at top)
  ├─ div
  └─ div (scrollable container)
       └─ div[role="grid"] (63+ children — virtualized list)
            ├─ div[role="row"] (chat item 1)
            ├─ div[role="row"] (chat item 2)
            └─ ...

Finding the Chat List

// DOM Way (CORRECT)
const grid = document.querySelector('[role="grid"]');
const rows = grid.querySelectorAll('[role="row"]');
// Store API Way (FORBIDDEN — documented for reference only)
// ⚠️ DO NOT USE — detectable by WhatsApp
window.Store.Chat.getModelsArray()

Virtualized List

The chat list is virtualized — only visible rows are in the DOM. Each row has inline styles for positioning:

<div role="row" style="z-index: 249; transition: none; height: 76px; transform: translateY(0px);">

Scrolling loads more rows. To access chats not currently rendered, scroll the list.

Chat Row Structure

Each [role="row"] contains:

<div role="row" style="z-index: N; height: 76px; transform: translateY(Npx);">
  <div role="gridcell">
    <div tabindex="-1" aria-selected="false">
      <div class="_ak72 ...">
        <!-- Left side: Avatar -->
        <div class="_ak8n">
          <div class="_ak8h">
            <div style="height: 49px; width: 49px;">
              <img class="... _ao3e" src="https://pps.whatsapp.net/v/..." />
            </div>
          </div>
        </div>
        <!-- Right side: Name, timestamp, last message -->
        <div class="_ak8l">
          <!-- Top row: Name + Timestamp -->
          <div role="gridcell" aria-colindex="2" class="_ak8o">
            <div class="_ak8q">
              <span title="Contact Name" class="... _ao3e">Contact Name</span>
            </div>
            <div class="_ak8i">
              <span>02:50</span>  <!-- timestamp -->
            </div>
          </div>
          <!-- Bottom row: Last message + unread badge -->
          <div class="_ak8j">
            <div class="_ak8k">
              <span title="Last message text...">
                <span dir="ltr" class="... _ao3e">Last message text</span>
              </span>
            </div>
            <div role="gridcell" aria-colindex="1" class="_ak8i">
              <span aria-label="1 unread message">
                <span>1</span>
              </span>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

Extracting Data from Chat Rows

Contact Name

// DOM Way (CORRECT)
const nameSpan = row.querySelector('span[title]');
const name = nameSpan?.getAttribute('title');
// Also: nameSpan?.textContent
// Store API Way (FORBIDDEN)
// ⚠️ DO NOT USE
const chat = window.Store.Chat.get(wid);
chat.name || chat.formattedTitle

Avatar Image

// DOM Way (CORRECT)
const img = row.querySelector('img._ao3e');
// OR: row.querySelector('img[src*="pps.whatsapp.net"]')
const avatarUrl = img?.src;
// Returns CDN URL like: https://pps.whatsapp.net/v/t61.24694-24/...
// These are direct-download URLs — fetch them with standard HTTP
// Store API Way (FORBIDDEN)
// ⚠️ DO NOT USE
contact.getProfilePicThumb()
window.Store.ProfilePic.requestProfilePicFromServer(wid)

Timestamp

// DOM Way (CORRECT)
const timeSpan = row.querySelector('._ak8i span');
const timeText = timeSpan?.textContent; // "02:50", "Yesterday", "12/25/2025"

Last Message Preview

// DOM Way (CORRECT)
const msgSpan = row.querySelector('._ak8j span[dir="ltr"]._ao3e');
const lastMessage = msgSpan?.textContent;

Unread Count

// DOM Way (CORRECT)
const unreadSpan = row.querySelector('[aria-label*="unread"]');
const unreadText = unreadSpan?.getAttribute('aria-label'); // "1 unread message"
const unreadCount = parseInt(unreadSpan?.querySelector('span')?.textContent || '0', 10);

Chat Row Identity (wa_id)

Chat rows do NOT have a data-id attribute. The wa_id is stored in React fiber props.

// DOM Way (CORRECT) — Read React fiber props attached to DOM nodes
function getChatId(row) {
  const inner = row.querySelector('[tabindex="-1"]');
  const el = inner || row;
  const fiberKey = Object.keys(el).find(k =>
    k.startsWith('__reactFiber') || k.startsWith('__reactInternalInstance')
  );
  if (!fiberKey) return null;

  let fiber = el[fiberKey];
  for (let depth = 0; depth < 20 && fiber; depth++) {
    const props = fiber.memoizedProps || fiber.pendingProps;
    if (props && typeof props === 'object') {
      for (const key of Object.keys(props)) {
        const val = props[key];
        // Check for WID objects with _serialized property
        if (val && typeof val === 'object' && val.id && val.id._serialized) {
          return val.id._serialized; // e.g., "152665250123795@lid" or "639254548862@c.us"
        }
      }
    }
    fiber = fiber.return;
  }
  return null;
}

Found at: fiber.memoizedProps.chat.id._serialized (typically depth 3 from [tabindex="-1"] inner div)

// Store API Way (FORBIDDEN)
// ⚠️ DO NOT USE
window.Store.Chat.getModelsArray().map(c => c.id._serialized)

Full Chat List Scan

// DOM Way (CORRECT) — Single pass to get all visible chats
const grid = document.querySelector('[role="grid"]');
const rows = grid.querySelectorAll('[role="row"]');

const chats = Array.from(rows).map(row => {
  const nameSpan = row.querySelector('span[title]');
  const img = row.querySelector('img._ao3e, img[src*="pps.whatsapp.net"]');
  const unread = row.querySelector('[aria-label*="unread"]');
  const chatId = getChatId(row); // See function above

  return {
    chatId,
    name: nameSpan?.getAttribute('title') || null,
    avatarUrl: img?.src || null,
    unreadCount: parseInt(unread?.querySelector('span')?.textContent || '0', 10),
  };
});

Conversation Panel (#main)

Opening a Chat

To open a chat, click the chat row using Puppeteer’s native mouse click (JavaScript .click() does NOT work reliably):

// DOM Way (CORRECT) — Use Puppeteer's mouse.click with coordinates
const row = grid.querySelectorAll('[role="row"]')[index];
const clickTarget = row.querySelector('[tabindex="-1"]') || row;
const rect = clickTarget.getBoundingClientRect();
await page.mouse.click(rect.x + rect.width / 2, rect.y + rect.height / 2);
// Wait 2-3 seconds for the conversation to render

Important: JavaScript element.click() does NOT trigger WhatsApp Web’s navigation. You must use Puppeteer’s page.mouse.click() which dispatches real browser mouse events.

Structure

Once a chat is open, #main appears:

#main (891x768)
  ├─ div (data-asset-chat-background-beige — wallpaper/background)
  ├─ HEADER (891x64 — contact info bar at top)
  │    ├─ div (avatar 40x40)
  │    ├─ div (contact name + online status)
  │    └─ div (video call / voice call / menu buttons)
  ├─ div (891x640 — message area, scrollable)
  │    └─ div (message container)
  │         ├─ div[data-id="true_..."] (sent message)
  │         ├─ div[data-id="false_..."] (received message)
  │         └─ ...
  ├─ div (empty spacer)
  ├─ FOOTER (891x64, class="_ak1i" — message input area)
  └─ span (empty)

Conversation Header

// DOM Way (CORRECT) — Read the header
const header = document.querySelector('#main header');

// Contact name (the first non-icon span)
const nameSpan = header.querySelectorAll('span');
// nameSpan[0] = icon reference text (ignore)
// nameSpan[1] = contact name (e.g., "Mia YUM 21 Mandaue")
// nameSpan[2] = online status (e.g., "online", "last seen today at 03:03")

// Avatar from header
const headerImg = header.querySelector('img');
// headerImg.src = CDN URL (40x40), e.g., https://media-bkk1-1.cdn.whatsapp.net/v/...
// Store API Way (FORBIDDEN)
// ⚠️ DO NOT USE
window.Store.Contact.get(wid).name
window.Store.Contact.get(wid).pushname

Message Structure

Messages have data-id attributes with the format:

{fromMe}_{chatId}_{messageId}
  • true_152665250123795@lid_AC680F1C... — sent by us (fromMe = true)
  • false_152665250123795@lid_3EB0A4B... — received from contact (fromMe = false)
<!-- Text message (sent) -->
<div data-id="true_152665250123795@lid_ACD8B235..." class="_amjv xa0aww2">
  <!-- Message bubble -->
  <div>
    <span class="x1f6kntn xjb2p0i x8r4c90">Hi Mia, chas here from Cupid</span>
    <span class="x1rg5ohu x16dsc37">03:54</span>  <!-- timestamp -->
  </div>
</div>

<!-- Image message (sent) -->
<div data-id="true_152665250123795@lid_AC15E5C8..." class="_amjv xa0aww2">
  <img src="blob:https://web.whatsapp.com/abc123..." />  <!-- actual image -->
  <span class="x1rg5ohu x16dsc37">03:54</span>
</div>

<!-- System message -->
<div data-id="true_..._AC680F1C..." class="_amjv xscbp6u">
  <span class="x13faqbe _ao3e">Messages and calls are end-to-end encrypted...</span>
</div>

Extracting Messages

// DOM Way (CORRECT) — Read rendered messages
const main = document.querySelector('#main');
const msgElements = main.querySelectorAll('[data-id*="true_"], [data-id*="false_"]');

const messages = Array.from(msgElements).map(el => {
  const dataId = el.getAttribute('data-id');
  const fromMe = dataId.startsWith('true_');

  // Extract text content
  const textSpan = el.querySelector('.x1f6kntn, .selectable-text');
  const text = textSpan?.textContent || '';

  // Extract timestamp
  const timeSpan = el.querySelector('.x1rg5ohu.x16dsc37');
  const time = timeSpan?.textContent || '';

  // Check for media
  const imgs = el.querySelectorAll('img[src^="blob:"]');
  const hasMedia = imgs.length > 0;

  // System message check
  const isSystem = el.classList.contains('xscbp6u');

  return { dataId, fromMe, text, time, hasMedia, isSystem };
});
// Store API Way (FORBIDDEN)
// ⚠️ DO NOT USE
const chat = window.Store.Chat.get(wid);
const messages = chat.msgs.getModelsArray();
chat.fetchMessages({ limit: 50 })

Scrolling for More Messages

To load older messages, scroll the message container up:

// DOM Way (CORRECT) — Natural scroll behavior
const msgArea = document.querySelector('#main > div:nth-child(3)'); // The 640px tall message area
msgArea.scrollTop = 0; // Scroll to top to trigger loading older messages
// Wait 1-2 seconds for WA to load more messages
// Store API Way (FORBIDDEN)
// ⚠️ DO NOT USE
chat.msgs.loadEarlierMsgs(50)

Media Extraction

Images in Messages

Images in messages render as <img> elements with blob: URLs:

// DOM Way (CORRECT)
const msgEl = document.querySelector('[data-id="true_..."]');
const imgs = msgEl.querySelectorAll('img[src^="blob:"]');
// blob: URLs can be fetched: fetch(blobUrl).then(r => r.blob())

Avatar Images

Avatars in the chat list use CDN URLs that are directly downloadable:

// DOM Way (CORRECT) — Avatar from chat list
const img = row.querySelector('img._ao3e');
const cdnUrl = img?.src;
// https://pps.whatsapp.net/v/t61.24694-24/584442654_...jpg?stp=dst-jpg_s96x96...
// These URLs return 96x96 JPEG images. Fetch with standard HTTP GET.

For the full-size avatar (128x128), it appears in the contact info panel header:

// DOM Way (CORRECT) — Large avatar from contact info panel
// 1. Click the contact name/avatar in the conversation header
// 2. Wait for the drawer panel to appear
// 3. Find the large avatar image
const drawerPanel = document.querySelector('[data-animate-drawer-right="true"]');
const largeAvatar = drawerPanel?.querySelector('img[style*="128"]');
// OR: the first img with width >= 100 in the drawer

Contact Info Panel

The contact info panel opens when you click the contact name or avatar in the conversation header.

Opening the Panel

// DOM Way (CORRECT)
const header = document.querySelector('#main header');
const clickable = header.querySelector('div[role="button"]')
  || header.querySelector('img')?.parentElement;
clickable.click(); // This usually works (unlike chat rows)
// Wait 2 seconds for panel to render

Closing the Panel

// DOM Way (CORRECT) — Press Escape
await page.keyboard.press('Escape');

Panel Contents

The contact info panel contains:

  • Large avatar image (128x128 CDN URL)
  • Contact name
  • Phone number
  • “About” text
  • Media/Links/Docs tabs
  • Shared groups
  • Encryption info

Note: The selector for the panel varies. Try:

document.querySelector('[data-animate-drawer-right="true"]')

Phone Numbers

@lid vs @c.us

WhatsApp uses two ID formats:

  • @lid (Linked ID): Internal linked device ID, e.g., 152665250123795@lid
  • @c.us (Phone): Real phone number, e.g., 639254548862@c.us

Many contacts appear as @lid in the chat list. The real phone number can be found:

  1. In the contact info panel (rendered as text)
  2. In React fiber props on the DOM node (via contact.phoneNumber.user)
// DOM Way (CORRECT) — Read from React fiber
// When scanning chat rows, the fiber at props.chat also contains:
// props.chat.contact.phoneNumber.user = "639150345543"
// This is accessible via the same fiber walking technique used for getChatId()
// Store API Way (FORBIDDEN)
// ⚠️ DO NOT USE
const contact = window.Store.Contact.get(wid);
contact.phoneNumber?.user  // Returns the real phone number
window.Store.Lid.unLid(wid) // Converts @lid to @c.us

CSS Class Reference

WhatsApp Web uses hashed/obfuscated class names that change between versions. However, some stable patterns:

Element Stable Selector Obfuscated Class
Avatar image img._ao3e _ao3e
Contact name span[title]._ao3e _ao3e
Chat row [role="row"] _ak72
Avatar container _ak8n, _ak8h
Chat content area _ak8l
Timestamp area _ak8i
Message name+preview _ak8o, _ak8j
Message bubble _amjv
Sent message xa0aww2
System message xscbp6u
Footer input _ak1i

Prefer role/attribute selectors ([role="row"], span[title], [data-id], [aria-label*="unread"]) over class names, as classes change between WA Web versions.


Going back to chat list from a conversation

// Press Escape or click the back button
await page.keyboard.press('Escape');

Scrolling the chat list to load more contacts

// The grid is virtualized — scroll to load more rows
const grid = document.querySelector('[role="grid"]');
grid.parentElement.scrollTop += 500; // Scroll down
// Wait 1 second for new rows to render

Important Notes

  1. Use Puppeteer page.mouse.click() for navigation, not JavaScript .click(). WhatsApp Web’s React event handlers may not respond to synthetic click events.

  2. Virtualized lists mean only ~60-70 chat rows exist in the DOM at any time, even if you have hundreds of chats. Scroll to load more.

  3. CDN URLs expire — avatar and media CDN URLs (pps.whatsapp.net, media-*.cdn.whatsapp.net) have authentication tokens in query params that expire. Download immediately.

  4. Wait after navigation — After clicking a chat or opening a panel, wait 2-3 seconds for WhatsApp to render the content before querying the DOM.

  5. All page.evaluate() calls that use document.querySelector* are reading the rendered DOM — this IS the correct approach. The forbidden methods are window.Store.* calls inside page.evaluate().

  6. React fiber reading is read-only — Walking the fiber tree on existing DOM nodes is passive inspection. We never call any React methods, setState, or modify the fiber tree.


Unmapped UI Areas

These areas exist in WhatsApp Web but are not yet fully documented. Run wa-explorer (POST /api/explorer/run) to produce a machine-readable map with screenshots.

Contact Info Panel — Tabs

The contact info drawer ([data-animate-drawer-right="true"]) contains multiple tabs/sections:

Tab Content Access
Media Shared images/videos grid Click “Media, Links and Docs” → Media tab
Links Shared URLs Click “Media, Links and Docs” → Links tab
Docs Shared documents Click “Media, Links and Docs” → Docs tab

These tabs render within the same drawer panel. The tab contents replace each other — only one is visible at a time.

Global “All Media” View

WhatsApp Web may expose a global media browser accessible from the left sidebar. This would show media across all conversations. The access path varies between WA Web versions — run the media-panel explorer to discover the current path.

Starred Messages Panel

Accessible via the sidebar menu or a dedicated icon. Shows all starred/bookmarked messages across conversations. Each entry links back to the original conversation.

Group Info Panel

When viewing a group chat, clicking the header opens a group info drawer with:

  • Group name and description
  • Member list with admin badges
  • Group settings (who can send messages, edit info)
  • Media/Links/Docs tabs (same as contact info)
  • Shared groups section is replaced by “Participants” section

Settings Panel

Accessible from the left sidebar (settings icon or menu → Settings). Contains:

  • Profile (name, about, avatar)
  • Account settings
  • Privacy settings
  • Notifications
  • Storage and data
  • Linked devices
  • Keyboard shortcuts

Status/Channels Tabs

The left sidebar contains tabs for:

  • Status[data-icon="status-v3-outline"] — View contacts’ status updates
  • Channels[data-icon="newsletter-outline"] — WhatsApp Channels feed
  • Communities[data-icon="community-outline"] — Community groups

Each tab replaces the chat list content in #pane-side.

Message Detail Elements

Within messages ([data-id]), these sub-elements may appear:

Element Selector Notes
Reactions Button with emoji text below message Not always present
Forwarded label [data-icon="forwarded"] Shows “Forwarded” tag
Read more Button containing “Read more” For truncated long messages
Tail icon [data-icon="tail-in"], [data-icon="tail-out"] Speech bubble pointer
Checkmarks [data-icon="msg-check"], [data-icon="msg-dblcheck"], [data-icon="msg-dblcheck-ack"] Sent/delivered/read
Star [data-icon="star"] or [data-icon="starred"] Bookmarked message