
Published on 18.03.2026

When you take a screenshot of a website using automation tools like Playwright or Selenium, the engine gives you exactly what is in the viewport: the website content itself.
However, many of our users want their screenshots to look "real." Whether it is for a landing page, a presentation, or a portfolio, a screenshot often looks better when it is wrapped in a browser window with buttons, a URL bar, and rounded corners.
The Challenge: There is no "Include Browser" flag
Because Playwright interacts directly with the browser engine, there is no simple setting to "capture the Chrome window." To solve this, I decided to build a custom "wrapper" using Python and the Pillow (PIL) library.
Instead of just saving the raw image, our API now:

The Code Behind the Frame
Below is the logic we use to "fake" the browser interface. We use ImageDraw to manually render the buttons and the URL field.
# --- helper functions ---
def prettify_url(url: str) -> str:
for prefix in ("https://", "http://"):
if url.startswith(prefix):
url = url[len(prefix):]
break
return url.rstrip("/")
def add_safari_frame(input_path, output_path, url):
screenshot = Image.open(input_path).convert("RGB")
width, height = screenshot.size
topbar_height = 78
corner_radius = 16
outer_bg = (242, 242, 242)
toolbar_bg = (246, 246, 246)
border = (210, 210, 210)
url_bg = (255, 255, 255)
text_color = (95, 95, 95)
icon_color = (120, 120, 120)
total_height = height + topbar_height
canvas = Image.new("RGBA", (width, total_height), (0, 0, 0, 0))
# Main browser body with rounded top corners
browser_layer = Image.new("RGBA", (width, total_height), (0, 0, 0, 0))
browser_draw = ImageDraw.Draw(browser_layer)
browser_draw.rounded_rectangle(
(0, 0, width - 1, total_height - 1),
radius=corner_radius,
fill=outer_bg + (255,),
outline=None
)
# Toolbar clipped into rounded outer shape
browser_draw.rectangle((0, 0, width, topbar_height), fill=toolbar_bg + (255,))
browser_draw.line((0, topbar_height - 1, width, topbar_height - 1), fill=border + (255,), width=1)
# Paste screenshot below toolbar
browser_layer.paste(screenshot.convert("RGBA"), (0, topbar_height))
# Shadow under toolbar
shadow_layer = Image.new("RGBA", (width, total_height), (0, 0, 0, 0))
shadow_draw = ImageDraw.Draw(shadow_layer)
shadow_y = topbar_height - 1
shadow_draw.rectangle((0, shadow_y, width, shadow_y + 6), fill=(0, 0, 0, 35))
shadow_layer = shadow_layer.filter(ImageFilter.GaussianBlur(radius=4))
canvas.alpha_composite(browser_layer)
canvas.alpha_composite(shadow_layer)
draw = ImageDraw.Draw(canvas)
def draw_chevron(x, y, direction="left", size=8, color=(120, 120, 120), width_line=2):
if direction == "left":
draw.line((x + size, y - size, x, y), fill=color, width=width_line)
draw.line((x, y, x + size, y + size), fill=color, width=width_line)
else:
draw.line((x, y - size, x + size, y), fill=color, width=width_line)
draw.line((x + size, y, x, y + size), fill=color, width=width_line)
def draw_plus(x, y, size=7, color=(120, 120, 120), width_line=2):
draw.line((x - size, y, x + size, y), fill=color, width=width_line)
draw.line((x, y - size, x, y + size), fill=color, width=width_line)
def draw_share_icon(x, y, w=16, h=14, color=(120, 120, 120), width_line=2):
draw.rounded_rectangle(
(x - w // 2, y - h // 2 + 4, x + w // 2, y + h // 2),
radius=3,
outline=color,
width=width_line
)
draw.line((x, y + 2, x, y - 8), fill=color, width=width_line)
draw.line((x, y - 8, x - 4, y - 4), fill=color, width=width_line)
draw.line((x, y - 8, x + 4, y - 4), fill=color, width=width_line)
def draw_download_icon_safari(x, y, color=(120, 120, 120), width_line=2):
r = 11
draw.ellipse((x - r, y - r, x + r, y + r), outline=color, width=width_line)
draw.line((x, y - 5, x, y + 4), fill=color, width=width_line)
draw.line((x, y + 4, x - 4, y), fill=color, width=width_line)
draw.line((x, y + 4, x + 4, y), fill=color, width=width_line)
def fit_text(text, font, max_width):
if draw.textlength(text, font=font) <= max_width:
return text
ellipsis = "..."
low, high = 0, len(text)
while low < high:
mid = (low + high) // 2
candidate = text[:mid].rstrip() + ellipsis
if draw.textlength(candidate, font=font) <= max_width:
low = mid + 1
else:
high = mid
return text[:max(0, low - 1)].rstrip() + ellipsis
# Fonts
try:
font = ImageFont.truetype("/System/Library/Fonts/Supplemental/Arial.ttf", 16)
except:
try:
font = ImageFont.truetype("DejaVuSans.ttf", 24)
except:
font = ImageFont.load_default()
cy = 39
# --- left traffic lights ---
dot_r = 6
dot_x = 20
dot_y = cy - dot_r
colors = [(255, 95, 86), (255, 189, 46), (39, 201, 63)]
for i, c in enumerate(colors):
x = dot_x + i * 18
draw.ellipse((x, dot_y, x + 2 * dot_r, dot_y + 2 * dot_r), fill=c)
# More spacing after traffic lights
nav_start_x = 90
draw_chevron(nav_start_x, cy, "left", size=7, color=icon_color, width_line=2)
draw_chevron(nav_start_x + 24, cy, "right", size=7, color=icon_color, width_line=2)
# --- right icons ---
# order: download, share, plus
right_margin = 22
icon_gap = 34
x_plus = width - right_margin - 6
x_share = x_plus - icon_gap
x_download = x_share - icon_gap
draw_download_icon_safari(x_download, cy, color=icon_color, width_line=2)
draw_share_icon(x_share, cy, color=icon_color, width_line=2)
draw_plus(x_plus, cy, size=6, color=icon_color, width_line=2)
# --- URL field ---
url_left = 145
url_right = x_download - 26
url_top = cy - 12
url_bottom = cy + 12
draw.rounded_rectangle(
(url_left, url_top, url_right, url_bottom),
radius=12,
fill=url_bg,
outline=border,
width=1
)
# Lock icon
lock_x = url_left + 14
lock_y = cy
draw.arc((lock_x - 4, lock_y - 9, lock_x + 4, lock_y - 1), start=180, end=360, fill=icon_color, width=2)
draw.rounded_rectangle((lock_x - 5, lock_y - 2, lock_x + 5, lock_y + 7), radius=2, outline=icon_color, width=2)
# URL text
shown_url = fit_text(prettify_url(url), font, url_right - (lock_x + 14) - 12)
draw.text((lock_x + 14, cy - 8), shown_url, fill=text_color, font=font)
# Final save as RGB
canvas.convert("RGB").save(output_path, quality=95)Why do it this way?
By drawing the interface with code rather than using a static background image, the browser frame stays perfectly crisp regardless of the screenshot resolution. Whether you are taking a 1280px or 4K screenshot, the buttons and URL text will always be pixel-perfect.
Get started with our Website Screenshot API on AWS or Azure Marketplace:
👉 AWS users: aws3.link/WebsiteScreenshotAPI
👉 Azure users: azr.link/WebsiteScreenshotAPI