Guides & Reference
X/Twitter DOM Selector Reference
Last verified: February 2026. Twitter frequently changes their DOM — always verify selectors before use.
This is the canonical reference for all known X/Twitter DOM selectors used across the XActions project. Check here before writing new selectors.
UserCell Elements
Search results, followers lists, following lists, and any user-list UI.
| Element |
Primary Selector |
Fallback Selector(s) |
Notes |
| User cell container |
[data-testid="UserCell"] |
[data-testid="user"], div[role="listitem"] |
Main container for user list items |
| Display name |
[data-testid="User-Name"] |
[data-testid="UserCell"] a[href^="/"] |
Note the hyphen: User-Name |
| Display name link |
[data-testid="User-Name"] a |
— |
Direct link to profile |
| Handle link |
[data-testid="User-Name"] a[tabindex="-1"] |
— |
The @username portion |
| Bio/Description |
[data-testid="UserDescription"] |
[dir="auto"]:not([data-testid]), [dir="auto"]:not([role]) |
⚠️ Case-sensitive: capital U |
| Follow indicator |
[data-testid="userFollowIndicator"] |
— |
"Follows you" badge |
| Follow button |
[data-testid$="-follow"] |
button[aria-label*="Follow @"] |
Uses suffix match ($=) |
| Unfollow button |
[data-testid$="-unfollow"] |
button[aria-label*="Following @"] |
Uses suffix match ($=) |
| Verified badge |
[data-testid="icon-verified"] |
svg[aria-label*="Verified"] |
|
| Protected icon |
[data-testid="icon-lock"] |
svg[aria-label*="Protected"] |
|
| Avatar container |
[data-testid="UserAvatar-Container"] |
— |
|
| Avatar image |
[data-testid="UserAvatar-Container"] img |
[data-testid*="UserAvatar"] img |
|
| User actions menu |
[data-testid="userActions"] |
button[aria-label="More"] |
The "…" menu on user cells |
| Scrollable cell |
[data-testid="cellInnerDiv"] |
— |
Generic inner div for scrollable lists |
| Internal profile link |
a[href^="/"][role="link"] |
a[href^="/"] |
Username extraction from cells |
Profile Page Elements
Selectors for profile pages (x.com/username).
| Element |
Primary Selector |
Fallback Selector(s) |
Notes |
| Profile username |
[data-testid="UserName"] |
— |
⚠️ No hyphen (unlike User-Name in tweets) |
| Profile header items |
[data-testid="UserProfileHeader_Items"] |
— |
Container for location, URL, join date |
| Profile schema |
[data-testid="UserProfileSchema"] |
— |
Structured profile data |
| Bio/Description |
[data-testid="UserDescription"] |
— |
Same selector as UserCell |
| Location |
[data-testid="UserLocation"] |
— |
|
| Website URL |
[data-testid="UserUrl"] |
— |
|
| Website link |
[data-testid="UserUrl"] a |
— |
The clickable link element |
| Join date |
[data-testid="UserJoinDate"] |
— |
|
| Birthday |
[data-testid="UserBirthday"] |
— |
|
| Avatar (profile page) |
[data-testid="UserAvatar"] img |
a[href$="/photo"] img |
|
| Avatar (unknown variant) |
[data-testid="UserAvatar-Container-unknown"] img |
— |
Appears in some contexts |
| Header photo |
a[href$="/header_photo"] img |
— |
Banner image |
| Profile image (generic) |
img[src*="profile_images"] |
— |
Matches by URL pattern |
| Default avatar |
img[src*="default_profile"] |
— |
Users with no profile pic |
| Followers link |
a[href$="/followers"] |
— |
|
| Followers count |
a[href$="/followers"] span |
— |
|
| Following link |
a[href$="/following"] |
— |
|
| Following count |
a[href$="/following"] span |
— |
|
| Verified followers |
a[href$="/verified_followers"] |
— |
|
| Premium badge |
[data-testid="premiumBadge"] |
— |
|
| Premium banner |
[data-testid="premiumBanner"] |
— |
|
| Premium link |
a[href*="premium"] |
— |
Premium upgrade link |
| Profile tab bar link |
[data-testid="AppTabBar_Profile_Link"] |
— |
Used for shadow-ban checking |
| Profile meta tag |
meta[property="al:android:url"] |
— |
Profile detection via meta tag |
Tweet Elements
Tweet/post containers, content, and metadata.
| Element |
Primary Selector |
Fallback Selector(s) |
Notes |
| Tweet container |
article[data-testid="tweet"] |
article[role="article"], [data-testid="cellInnerDiv"] article |
Primary tweet wrapper |
| Tweet text |
[data-testid="tweetText"] |
[lang][dir] span, article span[dir="auto"] |
|
| Social context |
[data-testid="socialContext"] |
— |
"Retweeted", "Pinned", "Replying to" |
| Tweet photo |
[data-testid="tweetPhoto"] |
— |
Photo container |
| Tweet image |
[data-testid="tweetPhoto"] img |
— |
Actual image element |
| Video player |
[data-testid="videoPlayer"] |
— |
|
| Quote tweet |
[data-testid="quoteTweet"] |
— |
|
| Nested quote tweet |
[data-testid="tweet"] [data-testid="tweet"] |
— |
Quote inside a tweet |
| Card/link preview |
[data-testid="card.wrapper"] |
— |
|
| Thread indicator |
[data-testid="tweet-thread-indicator"] |
— |
|
| Promoted/Ad tweet |
[data-testid="placementTracking"] |
— |
Filter these to skip ads |
| Tweet permalink |
a[href*="/status/"] |
— |
Link to individual tweet |
| Tweet timestamp |
a[href*="/status/"] time |
— |
|
| Timestamp element |
time |
— |
Generic time element |
| Tweet author avatar |
[data-testid="Tweet-User-Avatar"] |
— |
|
| Author name in tweet |
[data-testid="User-Name"] a[href^="/"] |
— |
Profile link from tweet |
Engagement Buttons
Like, reply, retweet, bookmark, share, and analytics buttons on tweets.
| Element |
Primary Selector |
Fallback Selector(s) |
Notes |
| Like button |
[data-testid="like"] |
button[aria-label*="Like"], [role="button"][data-testid*="like"] |
|
| Unlike button |
[data-testid="unlike"] |
button[aria-label*="Liked"] |
Already-liked state |
| Reply button |
[data-testid="reply"] |
button[aria-label*="Reply"] |
|
| Retweet/Repost button |
[data-testid="retweet"] |
button[aria-label*="Repost"] |
|
| Undo retweet |
[data-testid="unretweet"] |
button[aria-label*="Undo repost"] |
|
| Confirm retweet |
[data-testid="retweetConfirm"] |
— |
|
| Confirm undo retweet |
[data-testid="unretweetConfirm"] |
— |
|
| Bookmark button |
[data-testid="bookmark"] |
button[aria-label*="Bookmark"] |
|
| Remove bookmark |
[data-testid="removeBookmark"] |
button[aria-label*="Remove Bookmark"] |
|
| Share button |
[data-testid="share"] |
button[aria-label*="Share"] |
|
| More menu (caret) |
[data-testid="caret"] |
button[aria-label="More"], [data-testid="tweet"] button:last-of-type |
Three-dot menu |
| Hide reply |
[data-testid="hideReply"] |
— |
|
| Reply restriction |
[data-testid="replyRestriction"] |
— |
|
| Analytics button |
[data-testid="analyticsButton"] |
— |
|
| Impressions count |
[data-testid="impressions"] |
— |
|
Follow / Unfollow
| Element |
Primary Selector |
Fallback Selector(s) |
Notes |
| Follow button |
[data-testid$="-follow"] |
button[aria-label*="Follow @"] |
Suffix match — username prefix varies |
| Unfollow button |
[data-testid$="-unfollow"] |
button[aria-label*="Following @"] |
Suffix match |
| Unblock button |
[data-testid$="-unblock"] |
button[aria-label*="Unblock"], button[aria-label*="Blocked"] |
Suffix match |
| Unmute button |
[data-testid$="-unmute"] |
button[aria-label*="Unmute"], button[aria-label*="Muted"] |
Suffix match |
| Placement button |
[data-testid="placementTracking"] [role="button"] |
— |
Follow/Unblock variant in placements |
Compose / Post Creation
| Element |
Primary Selector |
Fallback Selector(s) |
Notes |
| Compose tweet button |
a[data-testid="SideNav_NewTweet_Button"] |
[data-testid="SideNav_NewTweet_Button"] |
Sidebar compose button |
| Tweet textarea (first) |
[data-testid="tweetTextarea_0"] |
[role="textbox"][data-testid], div[contenteditable="true"][role="textbox"] |
|
| Tweet textarea (any) |
[data-testid^="tweetTextarea_"] |
— |
Prefix match for threads |
| Tweet textarea (nth) |
[data-testid="tweetTextarea_${i}"] |
— |
Dynamic by index |
| Post/Tweet button |
[data-testid="tweetButton"] |
button[data-testid*="tweet"] |
|
| Inline tweet button |
[data-testid="tweetButtonInline"] |
— |
|
| Add thread "+" |
[data-testid="addButton"] |
— |
|
| Media file input |
[data-testid="fileInput"] |
input[data-testid="fileInput"] |
|
| Image file input |
input[data-testid="fileInput"][accept*="image"] |
— |
|
| Alt text input |
[data-testid="altTextInput"] |
— |
|
| Edit tweet |
[data-testid="editTweet"] |
— |
Premium feature |
| Delete tweet |
[data-testid="deleteTweet"] |
— |
|
| Quote tweet option |
[data-testid="quoteTweet"] |
— |
In retweet menu |
| Contenteditable textbox |
div[contenteditable="true"][role="textbox"] |
— |
Generic fallback |
Compose Toolbar
| Element |
Primary Selector |
Fallback Selector(s) |
Notes |
| Add poll |
[aria-label="Add poll"] |
[data-testid="pollButton"] |
|
| Add GIF |
[aria-label="Add a GIF"] |
[data-testid="gifyButton"] |
Note typo: gifyButton |
| Add emoji |
[aria-label="Add emoji"] |
[data-testid="emojiButton"] |
|
| Location button |
[data-testid="geoButton"] |
— |
|
Scheduling
| Element |
Primary Selector |
Fallback Selector(s) |
Notes |
| Schedule option |
[data-testid="scheduleOption"] |
[data-testid="scheduledButton"] |
|
| Date field |
[data-testid="scheduledDateField"] |
[data-testid="scheduleDateInput"] |
Two known variants |
| Time field |
[data-testid="scheduledTimeField"] |
[data-testid="scheduleTimeInput"] |
Two known variants |
| Schedule confirm |
[data-testid="scheduleConfirm"] |
[data-testid="scheduledConfirmationPrimaryAction"] |
|
Poll Elements
| Element |
Primary Selector |
Notes |
| Poll option inputs |
[data-testid="pollOption_0"] through [data-testid="pollOption_3"] |
0-indexed, max 4 |
| Poll text inputs (variant) |
[data-testid="pollOptionTextInput_0"] through [data-testid="pollOptionTextInput_3"] |
Alternate testid |
| Add poll option |
[data-testid="addPollOption"] |
[data-testid="addPollOptionButton"] variant |
| Poll duration |
[data-testid="pollDuration"] |
|
| Duration days |
[data-testid="pollDurationDays"] |
|
| Duration hours |
[data-testid="pollDurationHours"] |
|
| Duration minutes |
[data-testid="pollDurationMinutes"] |
|
| Remove poll |
[data-testid="removePoll"] |
|
| Poll results |
[data-testid="pollResults"] |
|
| Poll percentage |
[data-testid="pollPercentage"] |
|
| Poll total votes |
[data-testid="pollTotalVotes"] |
|
| Poll time remaining |
[data-testid="pollTimeRemaining"] |
|
| Radio option |
[role="radio"] |
Used in polls and settings |
| Poll group |
[role="group"] |
Container for poll options |
Profile Editing
| Element |
Primary Selector |
Notes |
| Edit profile button |
[data-testid="editProfileButton"] |
|
| Save profile |
[data-testid="Profile_Save_Button"] |
|
| Edit avatar |
[data-testid="editProfileAvatar"] |
|
| Edit header |
[data-testid="editProfileHeader"] |
|
| Add avatar button |
[data-testid="addAvatarButton"] |
|
| Add banner button |
[data-testid="addBannerButton"] |
|
| Cancel button |
[data-testid="cancelButton"] |
|
| Sort by latest |
[data-testid="sortByLatest"] |
|
| Sort by liked |
[data-testid="sortByLiked"] |
|
| Name input field |
input[name="displayName"] |
Used in edit profile modal |
| Bio textarea |
textarea[name="description"] |
Used in edit profile modal |
| Location input |
input[name="location"] |
Used in edit profile modal |
| Website URL input |
input[name="url"] |
Used in edit profile modal |
Navigation / Layout
| Element |
Primary Selector |
Fallback Selector(s) |
Notes |
| Back button |
[data-testid="app-bar-back"] |
button[aria-label="Back"] |
|
| Primary column |
[data-testid="primaryColumn"] |
— |
Main content area |
| Sidebar column |
[data-testid="sidebarColumn"] |
— |
Right sidebar |
| Timeline section |
section[role="region"] |
— |
|
| Tab list |
[role="tablist"] |
— |
|
| Individual tab |
[role="tab"] |
— |
|
| Active tab |
[role="tab"][aria-selected="true"] |
— |
|
| Main content |
[role="main"] |
— |
|
| Account switcher |
[data-testid="SideNav_AccountSwitcher_Button"] |
— |
|
| Sidebar avatar |
div[data-testid="SideNav_AccountSwitcher_Button"] img |
— |
|
| Cell inner div |
[data-testid="cellInnerDiv"] |
— |
Generic scrollable cell |
| Profile tab link |
[data-testid="AppTabBar_Profile_Link"] |
— |
Profile link in app tab bar |
Search / Explore
| Element |
Primary Selector |
Fallback Selector(s) |
Notes |
| Search input |
[data-testid="SearchBox_Search_Input"] |
— |
|
| Typeahead result |
[data-testid="TypeaheadListItem"] |
— |
Autocomplete suggestions |
| Typeahead user |
[data-testid="TypeaheadUser"] |
— |
User in autocomplete |
| Trend item |
[data-testid="trend"] |
[data-testid="trendItem"] |
Two known variants |
| Follow topic |
[data-testid="TopicFollow"] |
— |
|
Direct Messages
| Element |
Primary Selector |
Fallback Selector(s) |
Notes |
| New DM button |
[data-testid="NewDM_Button"] |
— |
|
| Search people |
[data-testid="searchPeople"] |
— |
DM people search |
| DM message input |
[data-testid="dmComposerTextInput"] |
[role="textbox"][data-testid*="dm"] |
|
| DM send button |
[data-testid="dmComposerSendButton"] |
button[data-testid*="send"] |
|
| Conversation item |
[data-testid="conversation"] |
— |
In DM list |
| Message bubble |
[data-testid="messageEntry"] |
— |
Individual message |
| Last message preview |
[data-testid="lastMessage"] |
— |
|
| Unread indicator |
[data-testid="unread"] |
— |
|
| Message reaction |
[data-testid="messageReaction"] |
— |
|
| Message requests tab |
[data-testid="messageRequests"] |
— |
|
| Accept request |
[data-testid="acceptRequest"] |
— |
|
| Delete request |
[data-testid="deleteRequest"] |
— |
|
| Next button |
[data-testid="nextButton"] |
— |
"Next" in DM compose flow |
| DM scroller |
[data-testid="DmScrollerContainer"] |
— |
Scrollable message container |
| DM conversation |
[data-testid="DMConversation"] |
— |
|
| DM drawer/inbox |
[data-testid="DMDrawer"] |
— |
|
| DM conversation info |
[data-testid="DMConversationInfoButton"] |
— |
|
| DM image button |
[data-testid="DMImageButton"] |
— |
|
| DM GIF button |
[data-testid="DMGifButton"] |
— |
|
| GIF result |
[data-testid="gif"] |
— |
|
| Reaction emoji |
[data-testid="reactionEmoji"] |
— |
|
| DM conversation (variant) |
[data-testid="DM_Conversation"] |
— |
Underscore-separated variant |
| DM conversation avatar |
[data-testid="DM_Conversation_Avatar"] |
— |
Avatar in DM conversation list |
| Sent message indicator |
[data-testid="messageSent"] |
— |
Indicates message was sent |
Block / Mute / Report
| Element |
Primary Selector |
Fallback Selector(s) |
Notes |
| Block option |
[data-testid="block"] |
[role="menuitem"] span:has-text("Block") |
In dropdown menu |
| Mute link |
[data-testid="muteLink"] |
[role="menuitem"] span:has-text("Mute") |
|
| Mute option |
[data-testid="mute"] |
— |
Variant testid |
| Unmute option |
[data-testid="unmute"] |
— |
|
| Report option |
[data-testid="report"] |
[role="menuitem"] span:has-text("Report") |
|
| Add muted word |
[data-testid="addMutedWord"] |
— |
|
| Muted word input |
[data-testid="mutedWordInput"] |
— |
|
| Save muted word |
[data-testid="saveMutedWord"] |
— |
|
| Report flow next |
[data-testid="ChoiceSelectionNextButton"] |
— |
Report wizard |
| Report reason options |
[role="radio"], [role="option"], button |
— |
Options in report/mute flows |
| Block inside menu |
[role="menuitem"] [data-testid="block"] |
— |
Block option nested in menu |
Notifications
| Element |
Primary Selector |
Notes |
| Notification item |
[data-testid="notification"] |
|
| Notification text |
[data-testid="notification"] span |
|
| Settings toggle |
[data-testid="settingsSwitch"] |
|
Bookmarks
| Element |
Primary Selector |
Fallback Selector(s) |
Notes |
| Clear bookmarks |
[data-testid="clearBookmarks"] |
[data-testid="clearAllBookmarks"] |
Two known variants |
| Bookmark folder |
[data-testid="bookmarkFolder"] |
— |
|
| Create folder |
[data-testid="createBookmarkFolder"] |
— |
|
| Confirm folder |
[data-testid="createFolderConfirm"] |
— |
|
| More options |
[aria-label="More"] |
— |
Shared with other contexts |
Lists
| Element |
Primary Selector |
Notes |
| Create list |
[data-testid="createList"] |
|
| List name input |
[data-testid="listNameInput"] |
|
| List description input |
[data-testid="listDescriptionInput"] |
|
| Private toggle |
[data-testid="listPrivateToggle"] |
|
| Save list |
[data-testid="listSaveButton"] |
|
| Add members |
[data-testid="addMembers"] |
|
| List container |
[data-testid="list"] |
|
| List item |
[data-testid="listItem"] |
|
| List description |
[data-testid="listDescription"] |
|
| Member count |
[data-testid="memberCount"] |
|
| Done/save assignment |
[data-testid="addToListsButton"] |
|
| Checkbox (list select) |
[role="checkbox"] |
|
| List links |
a[href*="/lists/"] |
|
| List heading |
h2[role="heading"] |
|
Communities
| Element |
Primary Selector |
Notes |
| Joined/leave button |
button[aria-label^="Joined"] |
Prefix match |
| Join button |
button[aria-label^="Join"] |
Prefix match |
| Pending request |
button[aria-label^="Pending"] |
Prefix match |
| Communities nav |
a[aria-label="Communities"] |
Sidebar navigation link |
| Community links |
a[href^="/i/communities/"] |
|
| Join community button |
button[data-testid="join"] |
|
| Community card |
div[data-testid="communityCard"] |
[data-testid="CommunityCard"] variant |
| Community header |
[data-testid="communityHeader"] |
|
Spaces / Live Audio
| Element |
Primary Selector |
Notes |
| Start Space button |
[data-testid="SpaceButton"] |
|
| Join Space |
[data-testid="joinSpace"] |
|
| Speaker list |
[data-testid="spaceSpeakers"] |
|
| Listener count |
[data-testid="spaceListeners"] |
|
| Recording indicator |
[data-testid="spaceRecording"] |
|
| Schedule Space |
[data-testid="scheduleSpace"] |
|
| Space topic |
[data-testid="spaceTopic"] |
|
| Space title |
[data-testid="spaceTitle"] |
|
| Space host |
[data-testid="spaceHost"] |
|
| Space card |
[data-testid="SpaceCard"] |
[data-testid="space"] variant |
| Scheduled Space |
[data-testid="scheduledSpace"] |
|
| Individual speaker |
[data-testid="spaceSpeaker"] |
|
| Live indicator |
[data-testid="spaceLive"] |
|
| Space bar |
[data-testid="SpaceBar"] |
|
| Space links |
a[href*="/i/spaces/"] |
|
| Space card media |
[data-testid="card.layoutLarge.media"] |
|
Grok AI
| Element |
Primary Selector |
Fallback Selector(s) |
Notes |
| Grok chat input |
[data-testid="grokInput"] |
textarea[placeholder*="Ask"], [contenteditable][role="textbox"] |
|
| Grok send button |
[data-testid="grokSendButton"] |
button[aria-label="Send"], button[data-testid*="send"] |
|
| Grok response area |
[data-testid="grokResponse"] |
[data-testid="grokResponseText"] |
|
| Grok response text |
[data-testid="grokResponseText"] |
— |
|
| New chat button |
[data-testid="grokNewChat"] |
a[href="/i/grok"] |
|
| Grok image gen |
[data-testid="grokImageGen"] |
— |
|
| Chat history |
[data-testid="grokChatHistory"] |
— |
|
| Any Grok element |
[data-testid*="grok"] |
— |
Wildcard match |
Articles / Longform
| Element |
Primary Selector |
Fallback Selector(s) |
Notes |
| Article title input |
[data-testid="articleTitle"] |
h1[contenteditable], [contenteditable][aria-label*="title" i] |
|
| Article body editor |
[data-testid="articleBody"] |
[data-testid="richTextEditor"], [contenteditable][role="textbox"] |
|
| Publish button |
[data-testid="articlePublish"] |
— |
|
| Save draft |
[data-testid="articleSaveDraft"] |
— |
|
| Cover image |
[data-testid="articleCoverImage"] |
— |
|
| Article list |
[data-testid="articleList"] |
— |
|
| Article card |
[data-testid="articleCard"] |
— |
|
| Article preview |
[data-testid="articlePreview"] |
— |
|
Creator Studio / Monetization
| Element |
Primary Selector |
Notes |
| Engagements stat |
[data-testid="engagements"] |
|
| Followers chart |
[data-testid="followersChart"] |
|
| Revenue tab |
[data-testid="revenueTab"] |
|
| Subscription settings |
[data-testid="subscriptionSettings"] |
|
| Tips settings |
[data-testid="tipsSettings"] |
|
| Export analytics |
[data-testid="exportAnalytics"] |
|
| Analytics card |
[data-testid="analyticsCard"] |
|
| Stat elements |
[data-testid*="stat"] |
Wildcard match |
| Metric elements |
[data-testid*="metric"] |
Wildcard match |
| Subscription info |
[data-testid="subscriptionInfo"] |
|
| Analytics tab |
[data-testid="analyticsTab"] |
|
| Boost button |
[data-testid="boostButton"] |
|
| Ads dashboard |
[data-testid="adsDashboard"] |
|
| Campaign list |
[data-testid="campaignList"] |
|
| Create campaign |
[data-testid="createCampaign"] |
|
| List item (stats) |
[role="listitem"] |
|
| Analytics nav link |
a[href="/i/account_analytics"] |
|
| Monetization settings |
a[href="/settings/monetization"] |
|
Settings / Privacy
| Element |
Primary Selector |
Notes |
| Toggle switch |
[role="switch"] |
|
| Protected tweets |
[data-testid="protectedTweets"] |
|
| Settings save |
[data-testid="settingsSave"] |
|
| Request data |
[data-testid="requestData"] |
|
| Settings links |
a[href^="/settings/"] |
|
| Option element |
[role="option"] |
|
| Account settings |
a[href="/settings/account"] |
|
| Privacy & safety |
a[href="/settings/privacy_and_safety"] |
|
| Notifications settings |
a[href="/settings/notifications"] |
|
| Download data |
a[href="/settings/download_data"] |
|
| Deactivate account |
a[href="/settings/deactivate"] |
|
| 2FA settings |
a[href="/settings/account/login_verification"] |
|
| Blocked accounts |
a[href="/settings/blocked"] |
|
| Muted accounts |
a[href="/settings/muted"] |
|
| Muted keywords |
a[href="/settings/muted_keywords"] |
|
| Content preferences |
a[href="/settings/content_preferences"] |
|
| Display settings |
a[href="/settings/display"] |
|
Dialogs / Confirmations
| Element |
Primary Selector |
Fallback Selector(s) |
Notes |
| Confirm button |
[data-testid="confirmationSheetConfirm"] |
[role="alertdialog"] button:nth-child(1) |
Unfollow, block, delete confirmations |
| Cancel button |
[data-testid="confirmationSheetCancel"] |
[role="alertdialog"] button:last-child |
|
| Modal container |
[data-testid="modal"] |
— |
|
| Sheet dialog |
[data-testid="sheetDialog"] |
— |
|
| Toast notification |
[data-testid="toast"] |
— |
|
| Alert element |
[role="alert"] |
— |
|
| Dropdown menu |
[data-testid="Dropdown"] |
— |
|
| Menu item |
[role="menuitem"] |
— |
Items inside dropdown menus |
Generic / Utility
Selectors used across many contexts as fallbacks or for text extraction.
| Element |
Primary Selector |
Notes |
| LTR text span |
[dir="ltr"] > span |
Display names, counts |
| LTR span (variant) |
[dir="ltr"] span |
Less specific |
| LTR element |
[dir="ltr"] |
Generic LTR container |
| Auto-dir (no testid) |
[dir="auto"]:not([data-testid]) |
Bio/description fallback |
| Auto-dir (no role) |
[dir="auto"]:not([role]) |
Bio/description fallback |
| Auto-dir generic |
[dir="auto"] |
Any auto-direction text |
| Internal link |
a[href^="/"] |
Any link within X |
| Internal link w/ role |
a[href^="/"][role="link"] |
|
| Status permalink |
a[href*="/status/"] |
Tweet links |
| Analytics link |
a[href*="/analytics"] |
|
| Likes tab |
a[href$="/likes"] |
|
| External links |
a[href*="t.co"] |
Shortened URLs |
| Premium link |
a[href*="premium"] |
|
| Follow-related links |
a[href*="/follow"] |
|
| Media images |
img[src*="media"] |
|
| Video elements |
video |
|
| SVG with testid |
svg[data-testid] |
|
| Heading element |
[role="heading"] |
|
| Generic send button |
button[aria-label="Send"] |
|
| Any button |
button, [role="button"] |
Generic button matching |
| Link with role |
a[role="link"] |
Accessible link element |
| Threads post container |
[data-pressable-container="true"] |
Meta Threads scraper only |
| Threads post fallback |
div[role="article"] |
Meta Threads scraper only |
| Markdown response |
[class*="markdown"] |
Grok markdown-formatted text |
| Compose article link |
a[href="/compose/article"] |
Navigate to article composer |
| Grok nav link |
a[href="/i/grok"] |
Navigate to Grok AI |
| Spaces nav link |
a[href*="/spaces"] |
Navigate to Spaces |
| Bookmarks nav link |
a[href="/i/bookmarks"] |
Navigate to bookmarks |
Common Pitfalls
Case Sensitivity
data-testid values are CASE-SENSITIVE:
- ✅
UserDescription (capital U, capital D)
- ❌
userdescription
- ✅
User-Name (capital U, capital N, with hyphen)
- ❌
user-name
- ⚠️
UserName on profile pages vs User-Name in tweets — they are different selectors
Suffix Matchers for Follow/Unfollow
Follow/unfollow buttons include a username prefix in their data-testid:
data-testid="nichxbt-follow"
data-testid="nichxbt-unfollow"
Always use the suffix match ($=):
// ✅ Correct
document.querySelector('[data-testid$="-follow"]');
// ❌ Wrong — won't match
document.querySelector('[data-testid="follow"]');
Bio vs Display Name in UserCells
Multiple [dir="auto"] elements exist inside a UserCell. To find the bio:
// ✅ Best approach — use data-testid
cell.querySelector('[data-testid="UserDescription"]');
// ⚠️ Fallback — filter out elements inside links
const bioEl = [...cell.querySelectorAll('[dir="auto"]')]
.filter(el => !el.closest('a') && !el.closest('[data-testid="User-Name"]'))
.pop();
Navigation Breaks Scripts
Browser console scripts stop when the page navigates. Use sessionStorage to persist state:
const getProcessed = () => {
try { return JSON.parse(sessionStorage.getItem('xactions_key') || '[]'); }
catch { return []; }
};
Confirmation Dialogs Are Required
Many destructive actions pop up a confirmation dialog. Always wait for and click it:
await sleep(500);
const confirm = document.querySelector('[data-testid="confirmationSheetConfirm"]');
if (confirm) confirm.click();
Promoted Content Filtering
Filter out ads before processing tweets:
const isAd = tweet.closest('[data-testid="placementTracking"]');
if (isAd) continue;
Rate Limiting
Add delays between actions (1–3 seconds minimum). X will temporarily restrict your account if you bulk-act too fast.
DOM Changes
X/Twitter frequently updates their DOM. If a selector stops working:
- Check the browser DevTools for the current
data-testid
- Look for
aria-label alternatives
- Fall back to structural selectors (
role, tag hierarchy)
- Update this document
Selector Architecture
The XActions codebase uses a fallback chain pattern for resilient selector matching.
Canonical Source Files
| File |
Purpose |
Selector Count |
src/utils/core.js |
Primary fallback-chain registry |
~29 keys |
src/automation/actions.js |
Action-oriented selectors |
~40+ keys |
src/automation/core.js |
Core automation selectors |
~20+ keys |
extension/content/injected.js |
Browser extension selectors |
~15 keys |
Fallback Chain Pattern
// From src/utils/core.js — the canonical pattern
const SELECTORS = {
tweet: [
'article[data-testid="tweet"]', // Primary: data-testid
'article[role="article"]', // Fallback 1: role
'[data-testid="cellInnerDiv"] article' // Fallback 2: structural
],
tweetText: [
'[data-testid="tweetText"]', // Primary
'[lang][dir] span', // Fallback 1
'article span[dir="auto"]' // Fallback 2
]
};
When writing new selectors:
- Primary: Use
data-testid when available
- Fallback 1: Use
aria-label or role attributes
- Fallback 2: Use structural/positional selectors as last resort
- Always add new selectors to this document
Contributing: When you discover a new selector or a changed one, update this file and the SELECTORS object in src/utils/core.js. Keep this document as the single source of truth.