This is a quick tutorial for web developers seeking for a way to display rasterized fallback in place of vector graphic elements for users browsing web with SVGs disabled, such as Tor browser on "safest" setting or Firefox with "svg.disabled" set to true in about:config.
The problem #

After I released my new website some of my friends reported that they couldn't use it because all icons are missing. The problem is that Tor browser, which is based on Firefox, does not render SVG elements at the "safest" security level.
The solution wasn't immediately obvious to me: <svg> elements are still there, not removed from DOM, so
- I can't use a CSS selector like svg + img { display: none }
- I also can't use picture tag because I inline all SVGs and want to control their color through currentColor property
- I also didn't want to replace all vector images with rasterized versions because it's considered a best practice to use SVG when possible
The SVG is there, in DOM, it's just not rendered by browser. I can't use JavaScript because it's blocked at this setting too and I can't detect whether SVG is rendered in any other way.
I asked for an advice on Reddit. My mistake, I shouldn't have done it. Most of people out there either have no idea what they're yapping about or want to be smarter than they are. Needless to say no one helped me.
The solution #
After a few hours of trial and error I settled on the only solution I could think of: overlaying a rasterized HiDpi rendered SVG with a real vector SVG icon.
Cons: #
- Not only it eliminates the main advantage of SVG โ serving 100-500 bytes instead of 1-5 kb, it doubles the size for each asset as you now have to serve both SVG and rasterized icon
- It creates additional overhead during build, maintanance and page loading
- It requires adding a build plugin and an image processor such as sharp
Pros: #
- It works when SVGs are blocked
- It does not require manually converting each SVG to raster
- You keep all perks of vector while making your website look good for everyone!
I had to write a Vite plugin that would automatically rasterize SVG icons for me. Here is my implementation I use in my personal website. Feel free to modify it if you want!
import type { Plugin } from "vite";
import fs from "fs/promises";
import sharp from "sharp";
function renderSvgPlugin(): Plugin {
return {
name: "render-svg",
transform: {
filter: {
id: /\.(svelte|svg)\?render$/,
},
order: "post",
async handler(_, id) {
const [filePath] = id.split("?");
const svg = await sharp(await fs.readFile(filePath));
const metadata = await svg.metadata();
const webp = await svg
.resize({
width: metadata.width * 2,
height: metadata.height * 2,
})
.webp({ quality: 90 })
.toBuffer();
const dataUrl = `data:image/webp;base64,${webp.toString("base64")}`;
return {
code: `export default ${JSON.stringify({
width: metadata.width,
height: metadata.height,
url: dataUrl,
})};`,
map: null,
};
},
},
};
}
export default renderSvgPlugin;
I use Svelte so I import icons from .svelte files but I also added .svg to filter matcher. I used transform hook with order: "post" to overwrite Svelte's Vite preprocessor.
Here is a TypeScript definitions file:
type SvgRenderResult = {
width: number;
height: number;
url: string;
};
declare module "*.svg?render" {
const svg: SvgRenderResult;
export default svg;
}
declare module "*.svelte?render" {
const svg: SvgRenderResult;
export default svg;
}
Here is how to use it. Note the ?render suffix which instructs Vite to
import TetherIcon from "$lib/icons/TetherIcon.svelte";
import TetherIconRendered from "$lib/icons/TetherIcon.svelte?render";
<SvgFallback {...TetherIconRendered}>
<TetherIcon />
</SvgFallback>
Now you can use the TetherIconRendered object with width, height and url properties to render the fallback image.
Here is my implementation of SvgFallback:
<script lang="ts">
let {
url,
width,
height,
children,
}: {
url: string;
width: number;
height: number;
children: import("svelte").Snippet;
} = $props();
</script>
<div
style="width: {width}px; height: {height}px;"
class="svg-fallback relative max-h-full"
>
{@render children()}
<img
src={url}
alt=""
class="absolute top-0 left-0 z-[-1] h-full w-full object-contain
object-center"
/>
</div>
<style>
.svg-fallback :global(svg) {
width: 100%;
height: 100%;
}
</style>