TikTok safe zone margins aren't FFmpeg pixels. Convert them.
Your caption burn-in looks right in VLC, then ships with the last line sitting under TikTok's own description block and the share rail eating the right edge of every wrapped word. The pixel margins that cause this are published in a dozen places. What almost nobody publishes is how to turn those numbers into FFmpeg arguments that survive a batch.
Quick answer: The TikTok safe zone on a 1080x1920 video is roughly 108 px from the top, 320 px from the bottom, 60 px on the left and 120 px on the right, and anything burned inside those bands can be covered by TikTok's own interface. To apply it in FFmpeg by hand, convert your subtitles to an.assfile whosePlayResX/PlayResYmatch the video, then style it withAlignment=2,MarginV=320,MarginL=120,MarginR=120so the margins are measured in real video pixels instead of libavcodec's scaled 384x288 default canvas. To skip re-deriving the coordinates on every clip, send the video to FFmpeg Micro's Auto Captions blueprint and get captions burned inside the safe zone as one call.
What the TikTok safe zone actually is
The TikTok safe zone is the part of a 1080x1920 frame that TikTok's own interface does not draw over: the username, caption text and sound ticker along the bottom, the like/comment/share/profile rail on the right, and the search and back controls at the top. It isn't a format spec and TikTok doesn't publish it as one. It's a measurement of where the app's chrome lands, which is why the numbers you find drift by a few dozen pixels depending on who measured them and which app build they measured on.
That matters more for burned-in captions than for anything else you composite. A watermark you place badly looks sloppy. A caption you place badly is unreadable, and the viewer's fix is to scroll.
Through mid-2026, these are the bands the creator-tool pages consistently publish, expressed against a 1080x1920 canvas:
| Platform | Top | Bottom | Left | Right |
|---|---|---|---|---|
| TikTok | 108 px | 320 px | 60 px | 120 px |
| Instagram Reels | 220 px | 420 px | 60 px | 180 px |
| YouTube Shorts | 150 px | 320 px | 60 px | 180 px |
Treat those as a floor, not gospel. Each app moves its own furniture a couple of times a year, and the right-hand rail in particular has grown as platforms added buttons.
One margin box that survives all three platforms
If the same clip goes to TikTok, Reels and Shorts, the useful move is to take the maximum of each side rather than maintaining three style presets: 220 px top, 420 px bottom, 60 px left, 180 px right. On a 1080x1920 frame that leaves a 840x1280 working area, which is still 53% of the canvas and plenty for two lines of 54 px caption text.
Expressed as fractions so they survive a resolution change: 11.5% off the top, 22% off the bottom, 5.6% off the left, 16.7% off the right. Percentages are the version worth memorizing, because a 720x1280 export needs 280 px of bottom clearance, not 420. Hardcoded pixel margins are the single most common reason a caption style that worked at 1080p ships broken at 720p.
Turning pixel margins into subtitle styles
The subtitles filter positions text through three ASS style fields: Alignment, MarginV, and the pair MarginL/MarginR. Alignment uses numpad geometry in ASS v4+, so 2 is bottom-center, 8 is top-center, and 5 is dead center. MarginV is the distance from whichever edge the alignment anchors to, so with Alignment=2 it pushes captions up from the bottom and with Alignment=8 it pushes them down from the top.
Why MarginV isn't measured in video pixels
The catch that wrecks the naive version: when you hand the subtitles filter a bare .srt, FFmpeg generates the ASS script for you, and libavcodec's default script canvas is 384x288. libass then scales that canvas onto your actual frame. So MarginV=320 against a 1920-tall video does not mean 320 video pixels, it means 320 units of a 288-tall canvas, which lands roughly 2,100 px up the frame and puts your captions somewhere near the ceiling.
The conversion, if you insist on staying with SRT, is margin_units = margin_pixels * PlayResY / video_height. For a 320 px bottom margin on a 1920-tall video that's 320 * 288 / 1920 = 48. This default has changed across FFmpeg releases, so verify the result with the check frame below rather than trusting the arithmetic on your build.
Build an .ass file with the canvas set correctly
The cleaner path is to stop scaling and own the canvas. Convert once, then the margins you write are the pixels you get:
ffmpeg -i captions.srt captions.ass
Open the result and set the script resolution to the video's real dimensions in [Script Info], then write the style line with the merged safe box:
[Script Info]
ScriptType: v4.00+
PlayResX: 1080
PlayResY: 1920
WrapStyle: 0
[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
Style: Safe,DejaVu Sans,54,&H00FFFFFF,&H000000FF,&H00000000,&H64000000,-1,0,0,0,100,100,0,0,1,3,0,2,180,180,420,1
Burn it in, overriding anything you want to tune per platform without re-editing the file:
ffmpeg -i input.mp4 \
-vf "subtitles=captions.ass:force_style='Alignment=2,MarginV=420,MarginL=180,MarginR=180,Fontsize=54,Outline=3,Shadow=0'" \
-c:v libx264 -crf 20 -preset medium -c:a copy output.mp4
One detail the margin tables never mention: with Alignment=2, libass centers text between MarginL and MarginR, so TikTok's asymmetric 60/120 pair shifts every centered caption 30 px left of true center. Set both sides to the larger value. Off-center captions read as a rendering bug even when the safe zone is respected.
Positioning drawtext hooks with y= expressions
Hook text and CTAs go through drawtext rather than the subtitle renderer, and drawtext positions in real frame pixels with no canvas scaling in between. Write the margins as fractions of h and the same command works at 1080x1920 and 720x1280:
ffmpeg -i input.mp4 -vf "drawtext=\
fontfile=/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf:\
text='Stop scrolling. This one is different.':\
fontsize=h/22:fontcolor=white:borderw=4:bordercolor=black:\
x=(w-text_w)/2:y=h*0.115" -c:a copy hook.mp4
For a line anchored above the bottom band instead, use y=h-text_h-(h*0.22). To center inside the asymmetric horizontal box rather than the full frame, the expression is x=(w*0.056)+((w*0.777)-text_w)/2, which centers between the 5.6% left margin and the 16.7% right margin.
Render a check frame before you publish a batch
Before a batch goes out, render one frame with the unsafe bands painted over the finished output. Overlap shows up as red-tinted text, and it takes about a second per clip:
ffmpeg -ss 00:00:05 -i output.mp4 -frames:v 1 -vf "\
drawbox=x=0:y=0:w=iw:h=ih*0.115:color=red@0.35:t=fill,\
drawbox=x=0:y=ih*0.78:w=iw:h=ih*0.22:color=red@0.35:t=fill,\
drawbox=x=0:y=0:w=iw*0.056:h=ih:color=red@0.35:t=fill,\
drawbox=x=iw*0.833:y=0:w=iw*0.167:h=ih:color=red@0.35:t=fill" \
safezone-check.png
Drop -frames:v 1, add -t 10, and write to check.mp4 if you want to watch a wrapped three-line caption grow into the top band during a fast segment. That's the failure the single frame misses.
This is also the point where doing it by hand stops paying. Every clip needs a transcript, a style block matched to its resolution, and a verification pass, and none of that is interesting work. FFmpeg Micro's Auto Captions blueprint is the automated version of everything above: transcribe, burn inside the safe box, review before download, no encoder to host and no fonts to install. The same job runs from the API or from an n8n, Make or Zapier step when it's one node in a bigger pipeline.
Common pitfalls
Most safe-zone bugs come from four places, and none of them are the margin numbers themselves.
- Alignment numbering silently changes. ASS v4+ uses numpad positions (2 = bottom-center, 8 = top-center). Legacy SSA
[V4 Styles]files use a different scheme where 5 and 9 mean top. A file converted from an old source can jump to the opposite edge with no error. - MarginV does nothing on middle alignments. With
Alignment=4,5or6, libass vertically centers the block and ignoresMarginVentirely. - Font substitution changes the block height. If the font named in your style isn't installed, fontconfig picks something else and your carefully measured two-line box becomes three lines. FFmpeg logs this as a non-fatal warning and keeps going, which is covered in FFmpeg fontconfig error isn't fatal. It ships the wrong font..
- Uploading anything other than 9:16. A 1080x1080 or 16:9 upload gets letterboxed or center-cropped by the app, which moves the interface relative to your pixels and invalidates every margin you set. Reframe first; the encode settings that survive platform re-encoding are in YouTube Shorts video settings that survive the re-encode.
When margins are the wrong fix
Safe-zone margins solve one problem: your own burned-in text colliding with app chrome. They don't help if the real issue is that you're burning captions at all. Platform-native captions reposition themselves as the interface changes and can never collide with it, so if you don't need branded typography, letting TikTok generate its own is a legitimate answer that costs you nothing to maintain.
Margins also can't rescue a design that needs per-word timing, animated keyframes or template-driven motion graphics. That's a template-editor category of tool, not an FFmpeg filter chain, and pretending otherwise produces an unmaintainable drawtext expression. Where FFmpeg wins is the styled, positioned, repeatable burn across hundreds of clips, which is the same job described in Auto-generate and burn in captions with Whisper + FFmpeg.
FAQ
What is the TikTok safe zone in pixels?
The TikTok safe zone on a 1080x1920 video reserves roughly 108 px at the top, 320 px at the bottom, 60 px on the left and 120 px on the right for TikTok's own interface. Keep captions, logos and CTAs inside the remaining area. TikTok doesn't publish these as an official spec, so round up rather than placing text right against the boundary.
Is the safe zone the same on Instagram Reels and YouTube Shorts?
Instagram Reels and YouTube Shorts both reserve more space than TikTok, mainly at the bottom and on the right-hand button rail. A single style that clears all three needs about 220 px top, 420 px bottom, 60 px left and 180 px right on a 1080x1920 frame, which is 11.5%, 22%, 5.6% and 16.7% of the canvas.
How do I move burned-in subtitles up in FFmpeg?
Raise MarginV in the subtitle style while keeping Alignment=2, for example subtitles=captions.ass:force_style='Alignment=2,MarginV=420'. The value is measured in the subtitle script's coordinate space, so set PlayResX/PlayResY in the .ass file to your video's real dimensions first, otherwise the number gets scaled by libass and lands somewhere you didn't intend.
Why does MarginV move my captions way too far?
A MarginV value that overshoots almost always means the subtitle script canvas doesn't match the video. FFmpeg generates ASS headers for plain .srt input using a 384x288 default canvas, and libass scales that up to your frame, multiplying a 1920-height margin by about 6.7. Convert to .ass and set the real resolution, or divide your pixel margin by that scale factor.
Do safe zones still matter if I export at 720x1280?
Safe zones matter at every resolution because the app's interface occupies a fixed fraction of the screen, not a fixed pixel count. A 320 px bottom margin at 1080x1920 becomes about 213 px at 720x1280. Write margins as percentages of frame height and width so one style block covers both exports.
Set the box once, verify it with a check frame, and the position problem stops recurring on every batch. If you'd rather not carry the style block and the font install into production at all, sign up free and run the same safe-zone caption burn as one call.
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

FFmpeg 8.0 Whisper Filter: Transcribe Video Without a Whisper API
The FFmpeg whisper filter transcribes video natively in FFmpeg 8.0, but only in a build compiled with --enable-whisper. The command, models, and production gaps.

Stop downloading from S3. Give FFmpeg a presigned URL.
FFmpeg can read an S3 presigned URL directly over HTTP range requests. The ffmpeg s3 presigned url flags, the HEAD 403, moov placement, and PUT output.

Your AI Dubbing Workflow Isn't Broken. The Dub Just Runs Long.
An AI dubbing workflow returns a translated track longer than the video. Measure both with ffprobe, fix the drift with atempo or apad, then re-mux cleanly.
Skip the command line
The Auto Captions blueprint transcribes your video and burns the captions in. You just review the transcript.
Run it (free)