How to Serve Transparent WebP Images Based on Browser Support
Serve transparent WebP with a real PNG fallback using <picture>, or negotiate formats with Accept and Vary: Accept. Includes runnable examples and cache guidance.
Serve the same transparent artwork as WebP and keep a real PNG fallback. For pages you control, use <picture> with a WebP source and a PNG <img>. If you need one stable image URL, negotiate the format from the request’s Accept header and return the matching bytes and Content-Type. For that server-side approach, send Vary: Accept and ensure every cache in front of the server respects it.
WebP supports alpha transparency in both lossy and lossless images, so transparency alone does not require PNG. The format decision still needs a fallback, correct response metadata, and cache behavior that matches the chosen representation. RFC 9649 defines WebP’s format capabilities.
1. Choose a delivery pattern
| Approach | Use it when | Trade-off |
|---|---|---|
<picture> |
You control the page markup and want the choice visible in HTML. | Different formats have different URLs; the fallback is explicit. |
| Server negotiation | You need a stable image URL and can configure your server and caches. | Correctness depends on content negotiation and cache keys. |
Start with <picture> unless a stable URL or centralized image delivery is important. Server-driven negotiation adds complexity: a server has incomplete information about the client, and varying representations can affect cache efficiency. There is no universally faster option; measure within your own delivery setup.
2. Browser-side selection with picture
Put the WebP source before the fallback image. The browser selects a source it can use; if it cannot use the WebP source, it falls back to the PNG img.
<picture>
<source srcset="/images/mark.webp" type="image/webp">
<img src="/images/mark.png" alt="Company mark" width="320" height="180">
</picture>
Both files should depict the same artwork, and the WebP file must actually retain its alpha channel. The filename extension does not prove that transparency survived encoding. Give the img useful alternative text, and include dimensions when known to reserve layout space.
For responsive artwork, you can provide multiple resolutions for each format:
<picture>
<source
type="image/webp"
srcset="/images/mark-320.webp 320w, /images/mark-640.webp 640w"
sizes="(max-width: 640px) 100vw, 320px"
>
<img
src="/images/mark-320.png"
srcset="/images/mark-320.png 320w, /images/mark-640.png 640w"
sizes="(max-width: 640px) 100vw, 320px"
alt="Company mark"
width="320"
height="180"
>
</picture>
The sizes value should reflect the rendered layout. Avoid generating many variants without a reason: each variant can increase storage and complicate caching.
3. Server-side negotiation with Accept
For a single URL, inspect the request’s Accept value for the explicit image/webp media range. Do not treat a broad */* wildcard alone as proof of WebP support. Browser header values can differ by request context, so make the decision from the image request itself rather than a guessed browser name.
Here is a runnable minimal Node.js server. Place mark.webp and mark.png in the same directory, then save this as server.mjs and run node server.mjs. It serves the two representations at /images/mark.
import { createServer } from 'node:http';
import { readFile } from 'node:fs/promises';
const files = {
webp: new URL('./mark.webp', import.meta.url),
png: new URL('./mark.png', import.meta.url),
};
function acceptsWebP(accept = '') {
return accept.split(',').some((part) => {
const [mediaType, ...parameters] = part.trim().toLowerCase().split(';');
if (mediaType.trim() !== 'image/webp') return false;
const q = parameters
.map((parameter) => parameter.trim())
.find((parameter) => parameter.startsWith('q='));
return !q || Number(q.slice(2)) > 0;
});
}
createServer(async (req, res) => {
if (req.url !== '/images/mark') {
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('Not found');
return;
}
try {
const useWebP = acceptsWebP(req.headers.accept);
const body = await readFile(useWebP ? files.webp : files.png);
res.writeHead(200, {
'Content-Type': useWebP ? 'image/webp' : 'image/png',
'Vary': 'Accept',
'Cache-Control': 'public, max-age=3600',
});
res.end(body);
} catch (error) {
console.error(error);
res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('Image unavailable');
}
}).listen(3000, () => {
console.log('Listening on http://localhost:3000');
});
The example recognizes an explicit image/webp entry and rejects a zero quality value. A production parser should correctly handle the full media-range syntax and quality parameters used by your clients. You can also use a framework or image service with documented negotiation behavior.
Vary: Accept tells caches that the response can differ based on the request’s Accept header. Verify the cache key at the CDN, reverse proxy, and any other shared cache, not only at the origin. If a cache ignores this distinction, it can deliver WebP bytes to a request expecting a fallback, or keep serving PNG to clients that accept WebP. See MDN’s content negotiation guide and web.dev’s image guidance.
4. Test both representations
Use cURL to send explicit request headers and inspect both the response headers and body type:
# Ask for WebP
curl -sS -D webp-headers.txt -H 'Accept: image/webp' \
http://localhost:3000/images/mark -o mark.webp
# Ask for the fallback
curl -sS -D png-headers.txt -H 'Accept: image/png' \
http://localhost:3000/images/mark -o mark.png
cat webp-headers.txt
cat png-headers.txt
Confirm that the first response has Content-Type: image/webp, the second has Content-Type: image/png, and both include Vary: Accept. Open or inspect the files with an image tool to confirm that the WebP still has transparency. Also test through the public CDN or proxy, since origin-only checks do not reveal cache-key mistakes.
5. Options, caching, and operational details
- Fallback format: PNG is a straightforward fallback for transparent artwork. Keep it available even if most of your traffic accepts WebP.
- URL strategy: Separate filenames make format and cache behavior easy to see. Negotiated URLs keep markup stable but require format-aware cache handling.
- Cache policy: For negotiated URLs, preserve
Vary: Acceptand verify the actual shared-cache key. For separate URLs, each representation has its own URL and can be cached independently. - Cache lifetime: The sample uses a one-hour public cache lifetime. Choose a lifetime that fits your asset update strategy; use versioned filenames or another invalidation method when content changes.
- Quality and file size: WebP can be lossy or lossless and supports alpha. Choose encoding settings for the image and inspect visual quality and resulting size; savings are not guaranteed for every asset.
- Variant count: Add responsive dimensions only where the layout benefits. Extra representations can make cache behavior less efficient.
- Reliability: Deploy both assets together, return the matching media type, and monitor missing-file and server errors. A failed WebP file should not result in mislabeled PNG bytes.
6. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| WebP image appears opaque | The encoded asset lost its alpha channel, or the artwork itself has an opaque background. | Re-export with alpha transparency and inspect the actual file, not just its extension. |
| Fallback request receives WebP | A shared cache reused a response without varying on Accept. |
Return Vary: Accept, configure the cache key to honor it, and purge incorrect cached objects. |
| Browser receives PNG every time | The request did not explicitly advertise WebP, the negotiation parser is too strict or incorrect, or an intermediary changed the request. | Inspect the image request’s Accept header and the public response. Do not infer support from a browser label. |
| Image fails to render | The response body and Content-Type disagree, or the selected file is missing or corrupt. |
Check status, media type, and file bytes for each branch; verify both assets exist in deployment. |
| Changes do not appear | A browser or CDN has an older cached representation. | Use versioned asset URLs or purge/update the cache according to your deployment policy. |
| Different formats appear inconsistently | The cache varies correctly at one layer but not another, or format negotiation is handled by multiple components. | Trace headers and cache keys from origin through the public edge; keep negotiation ownership clear. |
7. Or skip the browser setup
If you need a screenshot of a page containing transparent WebP artwork, ScreenshotNeo is a website screenshot API and MCP server. It captures the rendered page as PNG, JPEG, WebP, or PDF. The browser selects the image source or receives the negotiated response as part of normal page loading.
Make one request with a page URL; see the ScreenshotNeo API docs for options:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
- Cookie banners, newsletter popups, and chat widgets are removed before the shot; each cleanup step can be turned off.
- Bot checks, blank pages, failed loads, timeouts, and cache hits are never billed; response headers identify the page verdict and billing status.
- An MCP server lets AI agents use
take_screenshot,get_page_info, andcapture_pdf. - 1,000 screenshots per month are free with no card; paid plans start at $5 for 3,000 shots.
Sign up free for 1,000 screenshots a month, with no card required.
8. FAQ
Does transparency require lossless WebP?
No. WebP supports alpha transparency in lossy as well as lossless images. Choose encoding based on the appearance and file characteristics you need.
Can I use one URL for both formats?
Yes. Select the representation from Accept, return its matching Content-Type, and vary shared caches on Accept.
Should I detect the browser from its user-agent string?
No. Use the image request’s format negotiation or declare sources in <picture>; user-agent labels are not the format capability signal described here.
Which approach should I choose?
Use <picture> when you own the markup and want a visible fallback. Use server negotiation when a stable URL is valuable and you can verify the full cache path.


