Contents
Every vertical video app draws its own interface on top of your video: a search bar at the top, buttons down the right side, the caption and call to action at the bottom. Text that lands under those elements is hard to read, or hidden.
This guide gives the safe zone margins for TikTok, Instagram Reels, Stories and YouTube Shorts on a 1080 × 1920 frame, and the one box that clears all of them. Then it shows how to place titles and captions inside that box with the Shotstack Edit API, measured on the rendered frame.
TL;DR
- On a 1080 × 1920 frame, Google’s safe zone for vertical YouTube ads clears 288 px at the top, 672 px at the bottom, 48 px on the left and 192 px on the right.
- Meta’s guidance for Reels ads is 14% at the top, 35% at the bottom and 6% at each side: 269, 672 and 65 px.
- TikTok publishes template files instead of numbers, because its safe zone changes with the ad’s caption length and add-ons.
- Taking the largest margin on each side gives one box that clears all three: x 120 to 888, y 288 to 1248, or 768 × 960 px.
- In Shotstack, anchor a clip at
topLeft, setoffset.xtox0 / 1080,offset.yto-y0 / 1920and give it a pixel width and height.
Safe zone margins by platform
These are the margins on a 1080 × 1920 frame, in pixels, with the clear area they leave. For other aspect ratios and output sizes, see video sizes and aspect ratios by platform.
| Platform | Top | Bottom | Left | Right | Clear area |
|---|---|---|---|---|---|
| YouTube vertical ads (Google) | 288 (15%) | 672 (35%) | 48 (4.4%) | 192 (17.8%) | x 48 to 888, y 288 to 1248 (840 × 960) |
| Instagram and Facebook Reels ads (Meta) | 269 (14%) | 672 (35%) | 65 (6%) | 65 (6%) | x 65 to 1015, y 269 to 1248 (950 × 979) |
| TikTok In-Feed ads (third-party measurement of TikTok’s template) | 240 (12.5%) | 660 (34.4%) | 120 (11.1%) | 180 (16.7%) | x 120 to 900, y 240 to 1260 (780 × 1020) |
| All three combined | 288 | 672 | 120 | 192 | x 120 to 888, y 288 to 1248 (768 × 960) |
The combined row takes the largest margin on each side: the top and right from YouTube, the bottom from YouTube and Meta, and the left from TikTok. Text inside that box stays within all three platforms’ margins.
Why the numbers move
The margins are a baseline, not a fixed border. Google’s help page says “overlays, call-to-actions, and buttons may appear in different places depending on the format, campaign type, and screen.”
TikTok’s In-Feed ads help page says the safe zone depends on the ad’s dimensions, caption length and any extra formats. That is why TikTok publishes downloadable safe zone files, with separate versions for ads with anchors and for right-to-left Arabic layouts.
The TikTok numbers in the table come from a widely shared third-party measurement of the standard template. In it, TikTok’s right-hand button column is 180 px wide and starts 840 px from the top, with 120 px on the right above it.
Stories and organic posts
Third-party guides disagree on Meta’s bottom margin for Stories, giving either 20% or 35%. The Reels margin of 35% is the larger of the two, so the combined box clears Stories in both cases.
All three sets of numbers are published for ads. Use them for organic posts too, and check one real post on each app before you render a batch.
Convert pixel margins to Shotstack positions
Shotstack anchors a clip at the center of the frame by default. The safe box is not centered: its middle sits at (504, 768), up and to the left of the frame’s center at (540, 960). A wide caption left at center can run into the button column and the bottom bar.
Shotstack’s positioning guide anchors a clip to one of nine points, then moves it with offset. Offsets are fractions of the frame’s width and height: positive x moves right and positive y moves up.
The direct way to fill any pixel box (x0, y0, x1, y1) is to anchor the clip at topLeft and convert the box’s top-left corner:
position = "topLeft"
offset.x = x0 / 1080
offset.y = -y0 / 1920
width = x1 - x0
height = y1 - y0
offset.y is negative because moving down the frame from a topLeft anchor is a negative y. For the combined box, that gives offset.x 0.1111, offset.y -0.15, a width of 768 and a height of 960. The guide on how to position clips has more examples of anchors and offsets.
Render text inside the safe box with Python
The script below uses only Python’s standard library. It computes the combined box for the platforms in TARGETS, pins a title to the top of the box and a caption to the bottom, and adds a red overlay over the covered areas. Then it renders a 6-second 1080 × 1920 video with the Edit API.
import json
import os
import sys
import time
import urllib.error
import urllib.request
API = "https://api.shotstack.io/edit/v1/render"
W, H = 1080, 1920
# Pixels the app interface can cover on a 1080 x 1920 frame: top, bottom, left, right.
PLATFORMS = {
# Google Ads help, safe zone diagram for vertical video ads on YouTube
"youtube_shorts": (288, 672, 48, 192),
# Meta: keep 14% at the top, 35% at the bottom and 6% at each side clear
"reels_stories": (269, 672, 65, 65),
# Third-party measurement of TikTok's In-Feed safe zone template
"tiktok": (240, 660, 120, 180),
}
TARGETS = ["youtube_shorts", "reels_stories", "tiktok"]
BROLL_URL = "https://shotstack-assets.s3.amazonaws.com/footage/drone-city-flyover.mp4"
TITLE = "Inside the safe zone"
CAPTION = "Titles and captions stay clear of every app's buttons"
GUIDES = True # shade the covered areas red to check the layout
def safe_box(targets):
top, bottom, left, right = (max(PLATFORMS[t][i] for t in targets) for i in range(4))
return left, top, W - right, H - bottom
def placed(asset, box, length=6):
"""A clip that fills box exactly: anchored top left, offset as a share of the frame."""
x0, y0, x1, y1 = box
return {
"asset": asset,
"start": 0,
"length": length,
"position": "topLeft",
"offset": {"x": round(x0 / W, 4), "y": round(-y0 / H, 4)},
"width": x1 - x0,
"height": y1 - y0,
}
def text(words, size, vertical):
font = {"family": "Montserrat", "size": size, "weight": 800, "color": "#ffffff"}
align = {"horizontal": "center", "vertical": vertical}
# padding keeps descenders (g, p, y) inside the box when the text sits on an edge
return {
"type": "rich-text",
"text": words,
"font": font,
"align": align,
"padding": 16,
}
x0, y0, x1, y1 = box = safe_box(TARGETS)
print(f"Safe box: x {x0}-{x1}, y {y0}-{y1} ({x1 - x0} x {y1 - y0} px)")
tracks = [
{"clips": [placed(text(TITLE, 72, "top"), (x0, y0, x1, y0 + 300))]},
{"clips": [placed(text(CAPTION, 52, "bottom"), (x0, y1 - 300, x1, y1))]},
]
if GUIDES:
path = f"M0 0H{W}V{H}H0Z M{x0} {y0}V{y1}H{x1}V{y0}Z"
svg = (
f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {W} {H}" width="{W}" height="{H}">'
f'<path d="{path}" fill="#ff0000" fill-opacity="0.35" fill-rule="evenodd"/></svg>'
)
tracks.append(
{"clips": [{"asset": {"type": "svg", "src": svg}, "start": 0, "length": 6}]}
)
video = {"type": "video", "src": BROLL_URL, "volume": 0}
tracks.append({"clips": [{"asset": video, "start": 0, "length": 6, "fit": "crop"}]})
edit = {
"timeline": {"tracks": tracks},
"output": {"format": "mp4", "size": {"width": W, "height": H}},
}
def call(url, body=None):
data = json.dumps(body).encode() if body else None
key = os.environ["SHOTSTACK_API_KEY"]
request = urllib.request.Request(
url, data, {"x-api-key": key, "Content-Type": "application/json"}
)
try:
with urllib.request.urlopen(request) as response:
return json.load(response)["response"]
except urllib.error.HTTPError as error:
sys.exit(f"HTTP {error.code}: {error.read().decode()[:300]}")
if "--dry-run" in sys.argv:
print(json.dumps(tracks[0]["clips"][0], indent=2))
sys.exit()
if not os.environ.get("SHOTSTACK_API_KEY"):
sys.exit("Set SHOTSTACK_API_KEY first.")
render_id = call(API, edit)["id"]
print("Render queued:", render_id)
while True:
time.sleep(5)
status = call(f"{API}/{render_id}")
if status["status"] == "done":
print(status["url"])
break
if status["status"] == "failed":
sys.exit(f"Render failed: {status.get('error')}")
Get an API key, then run:
export SHOTSTACK_API_KEY="your-api-key"
python safe_zones.py
The script prints the box, queues the render and prints the video URL when it is done:
Safe box: x 120-888, y 288-1248 (768 x 960 px)
Render queued: 488f879e-9bd8-4d54-93ec-4e4e44cadd32
https://shotstack-api-v1-output.s3-ap-southeast-2.amazonaws.com/35tqpmb0ya/488f879e-9bd8-4d54-93ec-4e4e44cadd32.mp4
Run python safe_zones.py --dry-run to print the title clip’s JSON without rendering. This is the rendered video with GUIDES = True:
How the script places the text
safe_box()takes the largest margin on each side acrossTARGETSand returns the box as pixel coordinates. Remove a platform fromTARGETSand the box can grow.placed()turns a pixel box into a clip with thetopLeftanchor, the two offsets, and a pixelwidthandheight.- The title box is the top 300 px of the safe box (y 288 to 588). The caption box is the bottom 300 px (y 948 to 1248), so its
offset.yis -948 / 1920, or -0.4938. text()builds a rich-text asset. The title is aligned to the top of its box, so extra lines grow down. The caption is aligned to the bottom, so extra lines grow up and never cross y 1248.text()also sets 16 px ofpadding, so letters that drop below the line, such as g, p and y, stay inside the box.
The guide overlay is an SVG asset with one path: the full frame plus the safe box. With fill-rule="evenodd", the inner rectangle cuts a clear window through the red fill.
What the rendered frame shows
We rendered the script’s layout on a plain background and measured the pixels in the frame:
| Element | Configured box | Measured in the frame |
|---|---|---|
| Clear window in the red overlay | x 120 to 888, y 288 to 1248 | x 120 to 887, y 288 to 1247 |
| Title (Montserrat 72 px, weight 800, top) | x 120 to 888, y 288 to 588 | x 164 to 850, y 318 to 376 |
| Caption (Montserrat 52 px, weight 800, bottom) | x 120 to 888, y 948 to 1248 | x 140 to 856, y 1128 to 1242 |
The overlay’s window matches the box to the pixel, and both text blocks sit inside their boxes, centered within 3 px. With the padding, the title starts 30 px below the top of its box and the caption ends 6 px above the bottom.
Once the text looks right against the red overlay, set GUIDES = False and render the final video.
Keep every title readable with Shotstack
Work out the safe box once, from the platforms you post to, and let the code place every title and caption inside it. With the Shotstack video editing API, the same edit renders the same layout for every video in a batch.
Create a free Shotstack account and render your first vertical video with text that stays clear of the app interface.
Frequently asked questions (FAQs)
Do the margins change if I render at 720p or 4K?
The offsets don’t, because they are fractions of the frame. The pixel values do: multiply the margins, width and height by your output width divided by 1080. At 720 × 1280 that factor is 2/3, so the combined box becomes x 80 to 592, y 192 to 832.
Why does the script keep 180 px clear on the right for TikTok along the whole height?
In the measured template, the button column only starts 840 px from the top, with 120 px clear above it. One rectangle is easier to work with, and the larger margin costs only 60 px of width at the top.
If you need the full width for a title and TikTok is your only target, a title box above y 840 can use a right edge of 960 px.
Can I place auto-generated captions the same way?
Yes. A rich-caption clip takes the same position, offset, width and height, so you can pass it the caption box. Set fit to none as well, otherwise the caption clip covers the whole frame.