Ever taken a screenshot with Playwright or Selenium and felt like it looked a bit... empty?
When you automate screenshots, the browser engine captures exactly what’s in the viewport. It’s perfect for data, but if you’re using those images for a portfolio, a landing page, or a marketing report, they often look "naked" without the browser UI around them.
Recently, a customer of my Screenshot API asked for exactly this: "Can you make the screenshot look like it was actually taken in a browser?"
Since Playwright doesn't have a draw_browser_border=True flag, I had to build it myself using Python and the Pillow library. Here’s how I did it.
The Challenge: No Stretching Allowed
You can't just use a static background image of a browser and "stretch" it to fit. If your screenshot is 4K, a static UI image will look pixelated. If the aspect ratio changes, the buttons will look squashed.
The solution was to draw the browser UI programmatically so it stays pixel-perfect at any resolution.
The Implementation
We use ImageDraw to render the "traffic lights," the URL bar, and the icons. We also use a Gaussian blur to create a subtle shadow under the toolbar for that modern "layered" look.
from PIL import Image, ImageDraw, ImageFilter, ImageFont
# --- 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
# Safari-style color palette
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))
# 1. Create the 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,)
)
# 2. Add the Toolbar area and paste the screenshot
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)
browser_layer.paste(screenshot.convert("RGBA"), (0, topbar_height))
# 3. Apply a subtle shadow under the toolbar
shadow_layer = Image.new("RGBA", (width, total_height), (0, 0, 0, 0))
shadow_draw = ImageDraw.Draw(shadow_layer)
shadow_draw.rectangle((0, topbar_height - 1, width, topbar_height + 5), 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)
# --- Custom Drawing Logic for UI Elements ---
def draw_chevron(x, y, direction="left", size=7):
if direction == "left":
draw.line((x + size, y - size, x, y), fill=icon_color, width=2)
draw.line((x, y, x + size, y + size), fill=icon_color, width=2)
else:
draw.line((x, y - size, x + size, y), fill=icon_color, width=2)
draw.line((x + size, y, x, y + size), fill=icon_color, width=2)
# (Add your other drawing functions like draw_plus, draw_share_icon here)
# --- Draw the 'Traffic Lights' ---
for i, c in enumerate([(255, 95, 86), (255, 189, 46), (39, 201, 63)]):
x = 20 + i * 18
draw.ellipse((x, 33, x + 12, 45), fill=c)
# --- The URL Bar ---
url_left, url_right = 145, width - 120
draw.rounded_rectangle((url_left, 27, url_right, 51), radius=12, fill=url_bg, outline=border)
# Add the URL text (using fit_text to handle long URLs)
try:
font = ImageFont.truetype("Arial.ttf", 16)
except:
font = ImageFont.load_default()
clean_url = prettify_url(url)
draw.text((url_left + 30, 31), clean_url, fill=text_color, font=font)
# 4. Final Save
canvas.convert("RGB").save(output_path, quality=95)
How it works
- The Canvas: We create a new image that is slightly taller than the original to accommodate the 78px header.
- The Toolbar: We draw a solid rectangle and a 1px border.
- The Icons: Instead of using PNG icons, we use draw.line and draw.ellipse. This ensures the "back" button and "share" icons are perfectly sharp regardless of the image width.
- The Text: We use a helper function to strip the https:// and trailing slashes to make the address bar look like a real browser.
Conclusion
This was a fun weekend project that turned into a core feature for my users. If you're building an image-based tool, don't settle for raw exports—adding a little bit of UI "chrome" makes the output feel much more premium.
If you want to see it in action, you can test the feature directly in our live demo playground.
Top comments (0)