The favicon is the small image that appears in browser tabs, bookmarks, and search results. The ICO format, though old, remains essential because it is the only one that can contain multiple sizes in a single file.
ICO vs PNG vs SVG as favicon
| Format | Multiple sizes | Transparency | Vector | Universal support |
|---|---|---|---|---|
| ICO | Yes (single file) | Yes | No | 100% (IE included) |
| PNG | No (1 per file) | Yes | No | 95% |
| SVG | N/A (vector) | Yes | Yes | 80% (no IE) |
Modern strategy: ICO with 16×16 and 32×32 for compatibility + PNG 192×192 and 512×512 for PWA + SVG for modern browsers.
Create multi-size ICO with Pillow
from PIL import Image
def create_favicon_ico(source_image, ico_path, sizes=None):
"""
Create a multi-size ICO from any source image.
Source image should be at least 512×512 pixels.
"""
if sizes is None:
sizes = [16, 32, 48, 64, 128, 256]
img = Image.open(source_image)
if img.mode != 'RGBA':
img = img.convert('RGBA')
versions = [img.resize((s, s), Image.LANCZOS) for s in sizes]
versions[0].save(
ico_path,
format='ICO',
sizes=[(s, s) for s in sizes],
append_images=versions[1:],
)
print(f"ICO created: {ico_path} with sizes {sizes}")
create_favicon_ico('logo_512x512.png', 'favicon.ico')
create_favicon_ico('logo.png', 'favicon_simple.ico', sizes=[16, 32, 48])
Complete favicon pipeline for a website
from PIL import Image
from pathlib import Path
import json
def generate_all_favicons(source_image, output_dir='.'):
"""
Generates the complete favicon set for a modern website:
- favicon.ico (multi-size: 16, 32, 48)
- favicon-16x16.png, favicon-32x32.png
- apple-touch-icon.png (180x180)
- android-chrome-192x192.png, android-chrome-512x512.png
- og-image.png (1200x630)
"""
out = Path(output_dir)
out.mkdir(exist_ok=True)
img = Image.open(source_image)
if img.mode != 'RGBA':
img = img.convert('RGBA')
# Favicon ICO
ico_sizes = [16, 32, 48]
ico_imgs = [img.resize((s, s), Image.LANCZOS) for s in ico_sizes]
ico_imgs[0].save(
out / 'favicon.ico', format='ICO',
sizes=[(s, s) for s in ico_sizes],
append_images=ico_imgs[1:],
)
# PNG favicons
png_configs = [
('favicon-16x16.png', 16),
('favicon-32x32.png', 32),
('apple-touch-icon.png', 180),
('android-chrome-192x192.png', 192),
('android-chrome-512x512.png', 512),
]
for name, size in png_configs:
img.resize((size, size), Image.LANCZOS).save(out / name, format='PNG')
print(f" Created: {name} ({size}x{size})")
# Open Graph image (white background — OG doesn't use transparency)
og_size = (1200, 630)
og_img = Image.new('RGB', og_size, (255, 255, 255))
icon_size = min(og_size[1] - 100, 530)
icon_resized = img.resize((icon_size, icon_size), Image.LANCZOS)
x = (og_size[0] - icon_size) // 2
y = (og_size[1] - icon_size) // 2
og_img.paste(icon_resized, (x, y), icon_resized)
og_img.save(out / 'og-image.png', format='PNG')
print(f" Created: og-image.png (1200x630)")
print(f"\nFavicons generated in: {out.resolve()}")
generate_all_favicons('logo.png', 'public/')
Web Manifest (PWA icons)
import json
def generate_web_manifest(app_name, bg_color='#ffffff', theme_color='#000000'):
manifest = {
"name": app_name,
"short_name": app_name[:12],
"icons": [
{"src": "/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png"},
{"src": "/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png"},
{"src": "/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png",
"purpose": "maskable"},
],
"theme_color": theme_color,
"background_color": bg_color,
"display": "standalone",
"start_url": "/",
}
with open('site.webmanifest', 'w') as f:
json.dump(manifest, f, indent=2)
print("site.webmanifest generated")
generate_web_manifest("My App", theme_color="#1a73e8")
HTML: correct favicon setup
<head>
<!-- Classic favicon (universal compatibility) -->
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<!-- PNG for modern browsers -->
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<!-- Apple Touch Icon (iOS, Safari) -->
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<!-- SVG (very modern browsers — optional but recommended) -->
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<!-- PWA Manifest -->
<link rel="manifest" href="/site.webmanifest">
<!-- Theme color (Android Chrome address bar) -->
<meta name="theme-color" content="#1a73e8">
<!-- Open Graph -->
<meta property="og:image" content="https://yourdomain.com/og-image.png">
</head>
Verify favicon in production
curl -I https://yourdomain.com/favicon.ico
python3 -c "
from PIL import Image
img = Image.open('favicon.ico')
print('Sizes in ICO:', img.info.get('sizes', [(img.width, img.height)]))
"
Favicon production checklist
-
favicon.ico— 16×16, 32×32 (minimum) -
favicon-16x16.pngandfavicon-32x32.png -
apple-touch-icon.png— 180×180 -
android-chrome-192x192.pngandandroid-chrome-512x512.png -
site.webmanifestwith references to the PNGs -
og-image.png— 1200×630 for social media -
favicon.svg(optional, for very modern browsers)