How to Turn a Screen Recording into a GIF for Your README

You recorded a 30-second demo of your CLI, dragged the .mov into a web converter, and got back a 42 MB GIF that GitHub refused to upload. Or it uploaded, and now your README takes eight seconds to paint on a phone. The recording isn't the problem, and neither is the converter.
Quick answer: To convert a screen recording to GIF for a README, trim the dead air with-ssand-t, crop to the app window, drop the frame rate to 12 fps, and run FFmpeg's two-pass palette method:ffmpeg -ss 3 -t 12 -i screen.mov -filter_complex "[0:v]fps=12,scale=800:-1:flags=lanczos,split[a][b];[a]palettegen=stats_mode=diff[p];[b][p]paletteuse=dither=none:diff_mode=rectangle" demo.gif. That keeps a typical demo under GitHub's 10 MB image cap. If you'd rather not hand-tune four filters every release, FFmpeg Micro runs the same palette pipeline as one API call, with no FFmpeg install and no servers to run.
Conventional wisdom says a GIF is a lossy, low-quality format and there's only so small you can make one. That's half right. GIF has no interframe compression worth the name and a 256-color ceiling per frame, so it's a terrible container for video. But your 42 MB file isn't the format's fault. It's that macOS handed you a 2560x1600 capture at 60 frames per second, and you encoded every one of those pixels and frames into a file that renders 800 pixels wide in a browser. Four decisions get you a 20x reduction before the encoder does anything clever.
What GitHub actually accepts in a README
GitHub caps drag-and-drop image and GIF attachments at 10 MB. Files committed to the repo get more room (GitHub warns above 50 MB, blocks pushes above 100 MB), but that's the wrong target anyway. Every visitor to your repo page loads that GIF, autoplaying, with no lazy-load and no way to pause it. Aim for 2 to 5 MB. Under 2 MB is better.
A second constraint gets missed. GitHub proxies images through its camo service and caches them, so an externally hosted GIF doesn't dodge the size problem, it moves the latency. And the rendered width of a README image on github.com maxes out around 800 to 900 CSS pixels. Anything wider is downscaled by the browser, so you paid for pixels nobody sees.
Trim the dead air before anything else
Trimming is the single largest win because GIF file size scales almost linearly with frame count. Screen recordings start with two or three seconds of mouse-drift toward the terminal and end with you reaching for the stop button. Cut both.
ffmpeg -ss 3.5 -t 12 -i screen.mov -c copy trimmed.mov
-ss before -i is input seeking, which jumps to the nearest keyframe and is fast. On modern FFmpeg builds it's accurate for MP4 and MOV, but screen recordings sometimes have sparse keyframes. If your first frame is visibly wrong, move -ss after -i so FFmpeg decodes and discards instead of seeking, and accept the slower run.
Twelve seconds is a good ceiling for a README demo. If your feature takes longer to show, the demo is doing documentation's job and should be a linked video instead.
Crop to the app window, not the desktop
Cropping removes the desktop wallpaper, the menu bar, and the other half of your monitor, which cost bytes and tell the reader nothing. FFmpeg's crop filter takes width, height, and the x/y offset of the top-left corner:
ffmpeg -ss 5 -i screen.mov -frames:v 1 frame.png
Open that still, read the pixel coordinates of the window corners, and plug them in as crop=1600:1000:240:180. One trap: on a Retina display, the file is recorded at 2x the point coordinates your screenshot tool shows, so a window that looks 800 points wide is 1600 pixels in the file. Check the real dimensions first:
ffprobe -v error -select_streams v:0 \
-show_entries stream=width,height,r_frame_rate -of default=nw=1 screen.mov
cropdetect won't help here. It finds uniform black borders, not application windows, so a screencast on a light desktop gives it nothing to work with.
The palette pass, tuned for flat UI colors
The palette method is what separates a GIF that looks like your app from one that looks like a fax of your app. FFmpeg's default GIF encoder uses a fixed, generic 256-color palette. palettegen builds a palette from your actual frames, and paletteuse maps to it. The palette method walkthrough covers why the default looks so bad, down to the encoder internals.
For a screen recording, two flags matter that don't for camera footage:
ffmpeg -ss 3.5 -t 12 -i screen.mov -filter_complex \
"[0:v]fps=12,crop=1600:1000:240:180,scale=800:-1:flags=lanczos,split[a][b];\
[a]palettegen=max_colors=64:stats_mode=diff[p];\
[b][p]paletteuse=dither=none:diff_mode=rectangle" \
-loop 0 demo.gif
dither=none is the counterintuitive one. Every GIF tutorial tells you to use dither=bayer:bayer_scale=5, and for photographic content that's right. UI screencasts are mostly flat fills and antialiased text, so dithering adds visible grain to solid backgrounds, blurs small type, and inflates the file because noisy pixels compress badly. Turn it off and a terminal recording gets both sharper and smaller.
stats_mode=diff tells palettegen to weight colors in the parts of the frame that actually change, the right call when 80% of your capture is static window chrome. diff_mode=rectangle lets paletteuse only re-encode the changed rectangle per frame. max_colors=64 is usually invisible on a UI capture and shaves real bytes.
Filter order matters for speed. Put fps first so you're scaling 144 frames instead of 720.
What each knob costs you
You rarely need all five reductions, so it helps to know what each buys. A 24-second, 2560x1600, 60 fps macOS capture is the baseline.
| Knob | README setting | Rough effect on size |
|---|---|---|
| Duration | 12s instead of 24s | halves it |
| Frame rate | `fps=12` instead of 60 | roughly 5x smaller |
| Width | `scale=800:-1` from 2560 | ~10x fewer pixels per frame |
| Crop | app window only | whatever the desktop was, often 40% |
| Colors | `max_colors=64` | 15 to 25% on flat UI |
| Dither | `dither=none` | 20 to 40%, plus sharper text |
A 24-second terminal capture that starts north of 100 MB as a naive conversion lands around 4 MB with all of the above. If you're still over, run one more pass with gifsicle, which packs the final LZW better than FFmpeg's muxer:
gifsicle -O3 --lossy=80 --colors 64 demo.gif -o demo-small.gif
That typically takes another 30 to 50% off. --lossy on flat UI content shows up as faint speckle around text edges first, so eyeball it at 100% zoom.
Common pitfalls
Most of these failures are silent. The GIF still plays, it just looks wrong or weighs too much.
- Retina coordinates. Crop offsets read off a screenshot are half the real pixel values on a 2x display. Always
ffprobefirst. - The mouse cursor. macOS Cmd+Shift+5 records the pointer by default; QuickTime asks. A cursor jittering across an otherwise static frame defeats
diff_mode=rectangleand adds size for no benefit unless the demo is about clicking. -loop 0means loop forever,-loop -1means play once. The GIF muxer's numbering is the opposite of what most people guess.- Scaling to an odd width.
scale=801:-1can throw a "width not divisible by 2" error depending on the chain. Use even numbers. - Generating the palette on the untrimmed source. If you build the palette from 24 seconds and encode 12, you've reserved colors for frames that aren't in the output. Apply
-ss/-tto both passes, or use the single-commandsplitversion above. - Text that turns to mush. That's the dither, not the resolution. Set
dither=nonebefore you try scaling up.
When a GIF is the wrong format
A GIF stops being the right answer past about 15 seconds or whenever audio matters. At that length you're fighting the format, and no palette tuning saves you. GitHub renders .mp4 and .mov attachments as a real video player in a README when you upload the file through the web UI and use the resulting attachment URL. An H.264 MP4 of the same 24-second demo runs about 1.5 MB with a player, controls, and a scrub bar.
Use a GIF when the loop is short, silent, and you want it to autoplay inline. Use an MP4 for anything a reader might want to pause. If you're producing the MP4 too, the same trim-and-scale logic applies, with different targets, covered in compressing video for the web.
Automate it in your release flow
Regenerating a README GIF by hand every time the UI changes is how READMEs end up showing a version of your app from 2024. The fix is to make the GIF a build artifact: record the demo (or generate it from an end-to-end test run), drop the source video somewhere with a URL, and have a GitHub Actions job produce the GIF on release.
Installing FFmpeg in CI starts to bite there: you're pinning a build, waiting on apt-get, and debugging why the runner's FFmpeg lacks a filter. Sending the source URL to FFmpeg Micro makes the whole step one HTTP call from the workflow: submit the job, take the webhook, commit the returned GIF. The video-to-GIF blueprint is the same palette pipeline with length and width as options, if you'd rather not wire the filter chain yourself. It works the same way from n8n, Make, and Zapier, and from an AI agent over MCP. If you're generating GIFs for a whole docs site rather than one README, the batch transcode workflow is the pattern to copy.
FAQ
How do I convert a .mov screen recording to a GIF on a Mac?
You convert a .mov screen recording to a GIF on a Mac with FFmpeg's two-pass palette command, after installing FFmpeg via Homebrew (brew install ffmpeg) or a static build. The macOS install guide covers both. macOS records at your display's native resolution and refresh rate, so a Retina capture is typically 2560 pixels wide at 60 fps and needs both scale and fps before it's README-sized.
What size should a README GIF be?
A README GIF should be 800 pixels wide or less and under 5 MB. GitHub renders README images at roughly 800 to 900 CSS pixels and caps drag-and-drop image attachments at 10 MB. The file autoplays for every visitor with no way to pause it, so smaller is a real courtesy on mobile.
Why does my screen recording GIF look grainy?
A screen recording GIF looks grainy because of dithering, not resolution. The usual dither=bayer recommendation is tuned for photographic video; on flat UI colors it scatters noise across solid backgrounds and softens text. Set paletteuse=dither=none for screencasts and the grain disappears while the file gets smaller.
How do I cut the first few seconds off a screen recording?
Cut the start of a screen recording with -ss before the input and -t for the duration you want to keep: ffmpeg -ss 3.5 -t 12 -i screen.mov -c copy trimmed.mov. Input seeking is fast because FFmpeg jumps to a keyframe rather than decoding from the top, and -c copy avoids re-encoding entirely.
Can I use an MP4 in a GitHub README instead of a GIF?
You can use an MP4 in a GitHub README. Upload the .mp4 or .mov through GitHub's web UI and paste the resulting attachment URL into your Markdown, and GitHub renders a video player with controls. An MP4 is the better choice for anything longer than about 15 seconds or anything with audio, since H.264 has real interframe compression and GIF doesn't.
Recording the demo is the part only you can do. If you'd rather not maintain a filter chain and an FFmpeg install to get it into your README, sign up free and make the conversion one call from your release workflow.
About Javid Jamae
Founder & CEO at FFmpeg Micro
Javid is a software engineer, author, and entrepreneur with over 25 years of professional software development experience across enterprise, startup, and consulting environments. He founded FFmpeg Micro to make video processing accessible to developers through a simple, automation-first REST API.
You might also like

Convert MP4 to GIF with FFmpeg: The Palette Method That Doesn't Look Muddy
Convert MP4 to GIF with FFmpeg's palettegen and paletteuse two-pass method, control file size with fps and width, or run it as a free one-click blueprint.

FFmpeg Subtitle Delay Isn't a Slider Drag. Use -itsoffset
Fix ffmpeg subtitle delay with -itsoffset: shift an SRT by a constant, spot framerate drift that no offset fixes, and generate synced captions from the API.

Overlay images on video: skip the editor, use one API call
Overlay images on video with one API call or raw FFmpeg: logo stamps, lower thirds, picture-in-picture, timed fades, and the alpha and scale2ref gotchas.
Skip the command line
The Video to Animated GIF Converter blueprint runs the same conversion for you: upload the video, pick the length and width, download the GIF.
Run it (free)