Add Media to Posts -- Tutorial
Step-by-step guide to adding photos, GIFs, videos, and links to X/Twitter posts using XActions.
Prerequisites
- Logged into x.com in your browser
- Browser DevTools console (F12 -> Console tab)
- For Node.js usage:
npm install xactionsand a valid session cookie
Quick Start
- Navigate to x.com
- Open DevTools (F12)
- Use
src/postComposer.jsto post with media - Pass the
mediaoption with a local file path - Optionally add
altTextfor accessibility
Supported Media Types
| Type | Formats | Max Size | Max Count |
|---|---|---|---|
| Photos | JPG, PNG, WebP | 5MB each | Up to 4 per tweet |
| GIFs | GIF | 15MB | 1 per tweet |
| Videos | MP4, MOV | 512MB (free), 8GB (Premium+) | 1 per tweet |
| Links | Any URL | N/A | Automatic preview card |
Configuration
Node.js / Puppeteer (src/postComposer.js)
import { postTweet } from './src/postComposer.js';
// Post with a photo
await postTweet(page, 'Beautiful sunset today!', {
media: '/path/to/sunset.jpg',
altText: 'A golden sunset over the ocean with silhouetted palm trees',
});
| Option | Type | Default | Description |
|---|---|---|---|
media |
string |
null |
Absolute path to a local image, GIF, or video file |
altText |
string |
null |
Accessibility description for the media |
Step-by-Step Guide
Uploading Photos (Node.js / Puppeteer)
import { postTweet } from './src/postComposer.js';
// Single photo
await postTweet(page, 'Check this out!', {
media: '/home/user/photos/landscape.jpg',
altText: 'Mountain landscape with snow-capped peaks',
});
How it works internally:
- The script finds the hidden file input:
[data-testid="fileInput"] - Calls
uploadFile()on the input element with the local file path - Waits 2 seconds for X to process the upload
- If
altTextis provided, clicks[data-testid="altTextInput"]and types the description
Uploading Photos (Browser Console)
In the browser console, you cannot use uploadFile() directly. Instead, use the file input with DataTransfer:
// First, open the compose dialog and have it ready
// Then trigger the file picker:
const fileInput = document.querySelector('[data-testid="fileInput"]');
fileInput.click();
// This opens the native file picker -- select your image manually
Alternatively, you can drag and drop an image onto the compose box.
Adding GIFs
X has a built-in GIF picker. To use it programmatically:
// Browser console: open the GIF picker
document.querySelector('[aria-label="Add a GIF"]').click();
For Puppeteer, you can upload a GIF file the same way as a photo:
await postTweet(page, 'Mood today', {
media: '/path/to/reaction.gif',
});
Note: You can only attach 1 GIF per tweet, and you cannot combine a GIF with photos.
Uploading Videos
Videos work the same as photos through the file input:
await postTweet(page, 'Watch this demo!', {
media: '/path/to/demo.mp4',
});
Video processing takes longer than photos. The 2-second wait in postComposer.js may not be enough for large videos. For larger files, increase the wait time or check for the processing indicator.
Adding Links
Links are simply included in the tweet text. X automatically generates a preview card:
await postTweet(page, 'Great article on AI agents https://example.com/article');
- The URL counts toward your character limit
- X fetches Open Graph metadata from the URL to generate the card
- Preview cards may take a few seconds to appear
Media in Threads
When creating threads, you can attach media to individual tweets:
import { postThread } from './src/postComposer.js';
await postThread(page, [
{ text: 'Thread about our product launch!', media: '/path/to/hero.jpg' },
'Here is what we built and why...',
{ text: 'The architecture diagram:', media: '/path/to/diagram.png' },
'Thanks for reading! Let us know what you think.',
]);
Media Specifications
Photo Requirements
| Property | Requirement |
|---|---|
| Format | JPG, PNG, WebP |
| Max size | 5MB per image |
| Max images | 4 per tweet |
| Recommended | 1200x675px (16:9) for best display |
| Min size | 4x4 pixels |
Video Requirements
| Property | Free | Premium | Premium+ |
|---|---|---|---|
| Max length | 2 min 20 sec | 60 min | 3 hours |
| Max file size | 512MB | 2GB | 8GB |
| Format | MP4, MOV | MP4, MOV | MP4, MOV |
| Resolution | Up to 1920x1200 | Up to 1920x1200 | Up to 4K |
GIF Requirements
| Property | Requirement |
|---|---|
| Format | GIF |
| Max size | 15MB |
| Max per tweet | 1 |
| Cannot combine with | Photos or videos |
Tips & Tricks
- Alt text matters: Always add alt text for accessibility. It also helps with search visibility on X.
- Image optimization: Compress images before uploading. Tools like
sharporimagemincan reduce file sizes. - Video thumbnails: X auto-generates thumbnails. The first frame of your video becomes the preview.
- Multiple photos: X supports up to 4 photos per tweet. The Puppeteer
postTweetcurrently supports 1 media file -- calluploadFile()multiple times for more. - Link placement: Placing a link at the end of the tweet creates the cleanest layout with the preview card below the text.
- Media + polls: You cannot attach media and a poll to the same tweet (X limitation).
Troubleshooting
| Issue | Solution |
|---|---|
| File input not found | The selector [data-testid="fileInput"] may be hidden. It exists but is not visible in the DOM. |
| Upload seems stuck | Large videos take time to process. Wait for the progress bar to complete. |
| Alt text input not found | Click on the uploaded image thumbnail first -- the alt text button appears on the image preview. |
| Video too long | Free accounts are limited to 2:20. Upgrade to Premium for longer videos. |
| Image appears cropped | X crops images in the timeline. Use 16:9 aspect ratio for best results. |
| GIF not animating | The GIF may exceed size limits. Compress it or reduce the frame count. |
Related Scripts
src/postComposer.js-- Core posting with media supportsrc/videoCaptions.js-- Add captions/subtitles to videossrc/postThread.js-- Threads with media per tweetsrc/articlePublisher.js-- Long-form articles with cover images
⚡ Ready to try Add Media to Posts -- Tutorial?
XActions is 100% free and open-source. No API keys, no fees, no signup.
Browse All Scripts