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.
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 |
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/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 |
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"] | — | |
| 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 |
| 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 |
| 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"] | — | |
| 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"] | |
| 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 |
| 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 |
| 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 |
| 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"] | — | |
| 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 |
| 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 |
| Element | Primary Selector | Notes |
|---|
| Notification item | [data-testid="notification"] | |
| Notification text | [data-testid="notification"] span | |
| Settings toggle | [data-testid="settingsSwitch"] | |
| 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 |
| 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"] | |
| 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"] | |
| 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"] | |
| 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 |
| 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"] | — | |
| 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"] | |
| 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"] | |
| 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 |
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 |
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
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"]');
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();
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 []; }
};
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();
Filter out ads before processing tweets:
const isAd = tweet.closest('[data-testid="placementTracking"]');
if (isAd) continue;
Add delays between actions (1–3 seconds minimum). X will temporarily restrict your account if you bulk-act too fast.
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
The XActions codebase uses a fallback chain pattern for resilient selector matching.
| 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 |
// 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.