Guide
TikTok's own template keeps captions between y 240 and 1260 on a 1080x1920 video and gives 300 px on the right to its buttons from y 840. clipsdk burns its captions inside that area, or returns clean clips with SRT, for $0.03 per source minute.
TikTok safe zones for captions, beside Reels and YouTube
TikTok labels its safe zone template in 720x1280 units, so we measured the 2880x5120 file pixel by pixel for 1080x1920 and set it beside the 2 zones Meta and Google publish.
GuidesUpdated
Or one API call
$0.03per source minute
- Every clip included, up to 1080p60 and 20 Mb/s
- $1 free to start, 33 minutes of video
- Prepaid credit, no subscription
Safe zones on a 1080x1920 video
| Platform | Top | Bottom | Left | Right | Where it comes from |
|---|---|---|---|---|---|
| TikTok in-feed | 240 px | 660 px | 120 px | 120 px, and 300 px from y 840 down | Measured from TikTok's template |
| Instagram Reels | 268.8 px (14%) | 672 px (35%) | 64.8 px (6%) | 64.8 px (6%) | Meta's ads guide |
| YouTube vertical ads | 288 px | 672 px | 48 px | 192 px | Google Ads Help |
| All three at once | 288 px | 672 px | 120 px | 192 px, and 300 px from y 840 down | The strictest edge of each |
All three zones come from the platforms' ad guides. Meta gives percentages; the pixels above are those percentages of 1080x1920.
Measure the zone and check your captions
The scripts need Python with numpy and pillow, plus ffmpeg, and what each one printed sits under its code. The clips are one moment of NASA's Moon speech, rendered by clipsdk with each caption preset and with none.
Measure TikTok's zone from its template
TikTok's in-feed ads help page offers the safe zone as a zip of PNG overlays. The in-feed one is 2880x5120: coloured where the app draws its UI, transparent where your video shows. This script counts only fully transparent pixels and scales them to 1080x1920.
tiktok_zone.py import io import urllib.request import zipfile import numpy as np from PIL import Image TEMPLATE = "https://lf-tt4b.tiktokcdn.com/obj/i18nblog/tt4b_cms/en-US/5jqet0ab9qci-10e7f5Vig4uhAscNP8XPB0.zip" archive = zipfile.ZipFile(io.BytesIO(urllib.request.urlopen(TEMPLATE).read())) alpha = np.array(Image.open(archive.open("In-Feed/Feed.png")).convert("RGBA"))[:, :, 3] height, width = alpha.shape scale = 1920 / height print(f"template {width}x{height}, scaled to 1080x1920 below") # Fully transparent pixels are the safe area; the coloured overlay marks app UI clear = alpha == 0 rows = np.where(clear.sum(axis=1) > width // 4)[0] top, bottom = rows[0], rows[-1] + 1 print(f"top band: {top * scale:.0f} px, bottom band: {(height - bottom) * scale:.0f} px") # Walk down the safe area; short runs are the template's own labels, so we skip them runs = [] for y in range(top, bottom): xs = np.where(clear[y])[0] edges = (xs[0], xs[-1] + 1) if runs and runs[-1][2] == edges: runs[-1][1] = y + 1 else: runs.append([y, y + 1, edges]) bands = [] for start, end, edges in (r for r in runs if r[1] - r[0] >= 40): if bands and bands[-1][2] == edges: bands[-1][1] = end else: bands.append([start, end, edges]) for start, end, (left, right) in bands: print(f"y {start * scale:.0f}-{end * scale:.0f}: safe x {left * scale:.0f}-{right * scale:.0f}, " f"left band {left * scale:.0f} px, right band {(width - right) * scale:.0f} px") # $ python3 tiktok_zone.py # template 2880x5120, scaled to 1080x1920 below # top band: 240 px, bottom band: 660 px # y 240-840: safe x 120-960, left band 120 px, right band 120 px # y 840-1260: safe x 120-780, left band 120 px, right band 300 pxDownload a clip with and without captions
To find where captions land, you need the same moment twice: once with burned-in captions and once clean. These are 4 renders of one clip plus the clean clip's SRT.
fetch.sh for preset in bold:job_u21i20xzutohjd7s9ybd karaoke:job_4bklcp7t3u6bp21g5200 minimal:job_v7hv0qugdyl4ipqbftde none:job_dlbgy9meopo2hagbiojl; do curl -so ${preset%%:*}.mp4 https://api.clipsdk.com/f/out/${preset##*:}/clip-1.mp4 done curl -so none.srt https://api.clipsdk.com/f/out/job_dlbgy9meopo2hagbiojl/clip-1.srtFind the caption box in every frame
The two renders differ only where a caption was drawn, so a pixel diff 4 times a second gives the caption's box in each frame. The script then checks every box against all three zones.
caption_box.py import subprocess import sys import numpy as np W, H = 1080, 1920 # Safe areas as (left, top, right, bottom); TikTok's is an L, so it takes two boxes SAFE = { "TikTok": [(120, 240, 960, 840), (120, 240, 780, 1260)], "Reels": [(65, 269, 1015, 1248)], "YouTube": [(48, 288, 888, 1248)], } def frames(path): raw = subprocess.run(["ffmpeg", "-v", "error", "-i", path, "-vf", "fps=4", "-f", "rawvideo", "-pix_fmt", "gray", "-"], capture_output=True, check=True).stdout return np.frombuffer(raw, np.uint8).reshape(-1, H, W).astype(np.int16) def inside(box, area): return area[0] <= box[0] and area[1] <= box[1] and box[2] <= area[2] and box[3] <= area[3] captioned, clean = frames(sys.argv[1]), frames(sys.argv[2]) # The two renders differ only where a caption was drawn boxes = [] for a, b in zip(captioned, clean): changed = np.abs(a - b) > 64 rows = np.where(changed.sum(axis=1) > 8)[0] cols = np.where(changed.sum(axis=0) > 8)[0] if len(rows): boxes.append((cols[0], rows[0], cols[-1], rows[-1])) left, top = min(b[0] for b in boxes), min(b[1] for b in boxes) right, bottom = max(b[2] for b in boxes), max(b[3] for b in boxes) print(f"{sys.argv[1]}: caption in {len(boxes)} frames, x {left}-{right}, y {top}-{bottom}") for name, areas in SAFE.items(): outside = sum(not any(inside(box, area) for area in areas) for box in boxes) print(f" {name}: outside the safe zone in {outside} of {len(boxes)} frames") # $ for preset in bold karaoke minimal; do python3 caption_box.py $preset.mp4 none.mp4; done # bold.mp4: caption in 86 frames, x 134-753, y 872-1049 # TikTok: outside the safe zone in 0 of 86 frames # Reels: outside the safe zone in 0 of 86 frames # YouTube: outside the safe zone in 0 of 86 frames # karaoke.mp4: caption in 86 frames, x 124-772, y 1014-1221 # TikTok: outside the safe zone in 0 of 86 frames # Reels: outside the safe zone in 0 of 86 frames # YouTube: outside the safe zone in 0 of 86 frames # minimal.mp4: caption in 86 frames, x 147-748, y 1080-1241 # TikTok: outside the safe zone in 0 of 86 frames # Reels: outside the safe zone in 0 of 86 frames # YouTube: outside the safe zone in 0 of 86 framesBurn the SRT inside all three zones
Put the captions where every app leaves the frame clear: bottom edge at y 1248, x 120 to 780, with 12 px spare for the outline. The same check then finds the caption outside a zone in 0 of 86 frames.
burn_srt.py import re import subprocess # The area all three apps leave clear at 1080x1920, less room for the outline LEFT, RIGHT, BOTTOM, INSET = 120, 780, 1248, 12 def ass_time(srt_time): h, m, s = srt_time.replace(",", ".").split(":") return f"{int(h)}:{m}:{float(s):05.2f}" cues = re.findall(r"(\d+:\d+:\d+,\d+) --> (\d+:\d+:\d+,\d+)\n(.+?)(?:\n\n|\Z)", open("none.srt").read(), re.S) events = [f"Dialogue: 0,{ass_time(a)},{ass_time(b)},Caption,,0,0,0,,{' '.join(text.split())}" for a, b, text in cues] open("safe.ass", "w").write("\n".join([ "[Script Info]", "PlayResX: 1080", "PlayResY: 1920", "WrapStyle: 0", "", "[V4+ Styles]", "Format: Name, Fontname, Fontsize, PrimaryColour, OutlineColour, Bold, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV", f"Style: Caption,DejaVu Sans,56,&H00FFFFFF,&H00000000,1,1,5,0,2,{LEFT + INSET},{1080 - RIGHT + INSET},{1920 - BOTTOM + INSET}", "", "[Events]", "Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text", *events, ])) # Burn them into the clean clip subprocess.run(["ffmpeg", "-v", "error", "-y", "-i", "none.mp4", "-vf", "ass=safe.ass", "-c:v", "libx264", "-crf", "18", "-c:a", "copy", "safe.mp4"], check=True) print(f"{len(events)} captions inside x {LEFT}-{RIGHT}, above y {BOTTOM}: safe.mp4") # $ python3 burn_srt.py && python3 caption_box.py safe.mp4 none.mp4 # 5 captions inside x 120-780, above y 1248: safe.mp4 # safe.mp4: caption in 86 frames, x 145-750, y 1071-1239 # TikTok: outside the safe zone in 0 of 86 frames # Reels: outside the safe zone in 0 of 86 frames # YouTube: outside the safe zone in 0 of 86 frames
Get captioned clips and an SRT in one request
clipsdk's caption presets sit inside all three zones on 9:16, so you can ask for one. With karaoke, the talk came back as 3 ranked 9:16 clips with captions in under 2 minutes, each with an SRT and a VTT file. Ask for captions: false to burn your own with the step above.
The clip on the right is clip 1 of that request: 22 seconds of the speech, reframed to the speaker, with karaoke captions.
curl https://api.clipsdk.com/v1/clips \
-H "Authorization: Bearer $CLIPSDK_KEY" \
-H "Content-Type: application/json" \
-d '{
"video_url": "https://api.clipsdk.com/f/lib/demo/artemis-moon-speech.mp4",
"clips": 3,
"aspect": "9:16",
"captions": "karaoke"
}'How we read TikTok's template
TikTok's help page for in-feed ads has a Safe Zone section that says the zone depends on the video's size, the caption length and any extra formats, and offers the templates as downloads. The page itself gives no pixel numbers. The template does: its labels read 160 at the top, 440 at the bottom, 80 on each side and 120 for the button column, in 720x1280 units.
At 1080x1920 those labels come to 240, 660, 120 and 180 px, and our pixel count matches all four. The overlay fades out over a few pixels at its edges, so we counted only pixels with no overlay at all. The script skips the short runs of rows where the labels cross the clear area.
Where clipsdk captions land today
On 9:16, clipsdk keeps every caption inside one lane: x 120 to 780 and y 288 to 1248, the same area the burn step above uses. Bold sits in the middle of it, karaoke and minimal on its bottom edge.
Measured on the same clip, all three presets stayed inside TikTok's, Meta's and Google's zones in all 86 frames: bold at x 134 to 753 and y 872 to 1049, karaoke at x 124 to 772 and y 1014 to 1221, minimal at x 147 to 748 and y 1080 to 1241.
To place captions yourself, ask for captions: false and burn the SRT or VTT with the step above. See captions for the presets and files, and pricing for what a run costs.
Questions, answered
What is the TikTok safe zone?
On a 1080x1920 video, TikTok's own in-feed template leaves y 240 to 1260 clear, 120 px in from each side. From y 840 down the buttons take another 180 px on the right, so the clear area there is x 120 to 780.
What is the Instagram Reels safe zone?
Meta's ads guide says to leave at least 14% of the top, 35% of the bottom and 6% on each side free of text. On a 1080x1920 video that is 268.8 px at the top, 672 px at the bottom and 64.8 px on each side.
What is the YouTube safe zone for vertical video?
Google's safe zone for vertical video ads on YouTube keeps 288 px at the top, 672 px at the bottom, 48 px on the left and 192 px on the right of a 1080x1920 frame.
Where do captions fit all three?
Between y 288 and 1248, from x 120 to 888 above y 840 and from x 120 to 780 below it. We burned an SRT there, bottom edge at y 1248, and the caption left a zone in 0 of 86 frames.
Do clipsdk captions stay inside the safe zones?
Yes, on 9:16 clips every preset stays between x 120 and 780 and y 288 and 1248. On the clip above, bold, karaoke and minimal stayed inside the TikTok, Reels and YouTube zones in every frame.
Keep reading
- Animated captions, burned in word by wordBurn word-by-word captions into every clip in one of 3 presets: bold, karaoke or minimal. Or set captions to false for clean clips with SRT and VTT files. $0.03 per source minute.
- Build an OpusClip cloneThe whole clipping pipeline in 5 Python files: transcribe, pick moments with an LLM, reframe to 9:16, caption and render, with real costs.
- Clip every conference session the same dayUpload each conference session as it ends and get up to 20 ranked vertical clips per talk. A 40 minute keynote costs ~$1.20, with 3 jobs at once and 200 jobs a day.
- Whisper word timestampsWord-level times from faster-whisper, whisper.cpp, WhisperX and openai-whisper, cut to one clip and paged into karaoke captions.
- Active speaker detection in PythonFind who is talking in a two-person shot on a CPU with YuNet landmarks and mouth motion, then check it against the transcript.
- AutoFlip alternativeGoogle ended AutoFlip support in 2023. Rebuild its shot-by-shot vertical crop in Python with MediaPipe Tasks and PySceneDetect, or send one request.
Sources
- TikTok: in-feed ads help, Safe Zone, checked 7 November 2026
- TikTok: safe zone templates (zip), checked 7 November 2026
- Meta: Instagram Reels video ad specs, checked 7 November 2026
- Google Ads Help: universal safe zones for video ads on YouTube, checked 7 November 2026
- NASA: Bill Nelson on the 60th anniversary of the Moon speech, checked 7 November 2026
Start free
Your back catalog is full of shorts
Your first $1 is free: 33 minutes of video, every clip included up to 1080p60 and 20 Mb/s.
