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
See pricing

Safe zones on a 1080x1920 video

Safe zones on a 1080x1920 video
PlatformTopBottomLeftRightWhere it comes from
TikTok in-feed240 px660 px120 px120 px, and 300 px from y 840 downMeasured from TikTok's template
Instagram Reels268.8 px (14%)672 px (35%)64.8 px (6%)64.8 px (6%)Meta's ads guide
YouTube vertical ads288 px672 px48 px192 pxGoogle Ads Help
All three at once288 px672 px120 px192 px, and 300 px from y 840 downThe 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.

  1. 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 px
    
  2. Download 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.srt
    
  3. Find 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 frames
    
  4. Burn 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.

POST/v1/clipsTry it
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"
  }'
Clip 1, 22 seconds, with karaoke captions inside all three zones.clip-1.mp4clip-1.srtclip-1.vtt

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.

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.

Two studio microphones on a desk in a dark room lit by a red glow