Rive in a Chrome Extension (Manifest V3): Bundle the WASM
By the Charmotion team · Updated

Rive fails in a Manifest V3 extension because the web runtime downloads rive.wasm from unpkg by default, and Chrome bans remotely hosted code, WebAssembly included. Bundle @rive-app/canvas, copy its WASM files into the package, call RuntimeLoader.setWasmUrl() with chrome.runtime.getURL() before creating the animation, ship the .riv inside, and add 'wasm-unsafe-eval' to the CSP.
Key takeaways
- Chrome defines remotely hosted code as anything executed that is loaded from outside the extension's own files, and names WASM explicitly.
- The default extension CSP disables WebAssembly;
script-src 'self' 'wasm-unsafe-eval'; object-src 'self';is the policy you need. - The runtime fetches
rive.wasmfrom unpkg and a fallback from jsDelivr; override both with local URLs. - Keep the WASM files and the npm package on exactly the same version; Rive says a mismatch can break rendering.
- Electron and Tauri need the same self-hosted WASM plus
'wasm-unsafe-eval'in any CSP you set.
Already have a character or logo? See your character in real app screens: splash, onboarding, loading and empty-state screens. It's free, runs in your browser and uploads nothing.
Why won't Rive load in my Chrome extension?
Two Manifest V3 rules collide with the runtime's defaults.
1. Remote code is banned. Chrome defines remotely hosted code as "anything that is executed by the browser that is loaded from someplace other than the extension's own files," and lists "Things like JavaScript and WASM" as examples. Extensions must "bundle all code they are using inside the extension itself." Rive's web runtime, out of the box, downloads its engine from a CDN. In @rive-app/canvas 2.47.1 the default is https://unpkg.com/@rive-app/canvas@2.47.1/rive.wasm, and if that fails it tries rive_fallback.wasm from jsDelivr.
2. The default CSP disables WebAssembly. Without a content_security_policy key, extension pages get script-src 'self'; object-src 'self';, and Chrome's docs say that under it "WebAssembly will be disabled." The most permissive policy Chrome accepts for extension pages is script-src 'self' 'wasm-unsafe-eval'; object-src 'self';.
So the symptoms are either a blocked network request for rive.wasm or a CSP error about WebAssembly compilation, followed by a blank canvas. Fix both.
What CSP does the manifest need for WebAssembly?
Add 'wasm-unsafe-eval' to extension_pages. That is the minimum policy from Chrome's docs, so it is also the most you can relax it:
{
"manifest_version": 3,
"name": "Mascot",
"version": "1.0.0",
"action": { "default_popup": "popup.html" },
"content_security_policy": {
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';"
}
}
- Don't add
'unsafe-eval'or a CDN host; Chrome rejects policies looser than the minimum at install time. - This policy covers extension pages: the popup, side panel, options page and a new tab override.
- The .riv itself is fetched from the extension's own origin, which
'self'allows.
How do I bundle rive.wasm and the .riv in the extension?
Install an exact runtime version, copy its two WASM files into the extension folder, and point the runtime at them before the first animation is created. The npm package ships both files (in 2.47.1, rive.wasm is 2,165,388 bytes and rive_fallback.wasm 2,191,884 bytes).
npm install --save-exact @rive-app/canvas@2.47.1
mkdir -p extension/vendor
cp node_modules/@rive-app/canvas/rive.wasm extension/vendor/
cp node_modules/@rive-app/canvas/rive_fallback.wasm extension/vendor/
Then, in the popup's script (bundled with Vite, esbuild, Rollup or webpack, since extension pages can't resolve bare npm imports):
import { Rive, RuntimeLoader } from "@rive-app/canvas";
// Must run before the first new Rive(): no CDN requests from now on.
RuntimeLoader.setWasmUrl(chrome.runtime.getURL("vendor/rive.wasm"));
RuntimeLoader.setWasmFallbackUrl(chrome.runtime.getURL("vendor/rive_fallback.wasm"));
const canvas = document.getElementById("mascot");
const r = new Rive({
src: chrome.runtime.getURL("assets/character.riv"), // shipped in the package
canvas,
stateMachine: "CharacterViewModelMachine",
autoplay: !matchMedia("(prefers-reduced-motion: reduce)").matches,
autoBind: true,
onLoad: () => r.resizeDrawingSurfaceToCanvas(),
});
document.addEventListener("visibilitychange", () => {
if (document.hidden) r.pause(); else r.play();
});
setWasmUrlis the call Rive documents for self-hosting. Rive's docs add that the WASM version "must match the@rive-apppackage version," because the two are built as a pair; update the copied files in the same commit as any version bump.setWasmFallbackUrlexists in the runtime source from 2.40.1 on (we checked 2.40.1 through 2.47.1). Pointing it at the bundled fallback means even the retry stays local; on older versions the call doesn't exist.- Ship the .riv in the package and load it with
chrome.runtime.getURL(). It then works offline and there is nothing remote for a reviewer to ask about.
The library code still contains the default CDN URLs as strings, even though they are never used once you override them. Chrome's guide suggests searching built code for https:// to find violations, so if a reviewer raises it, point to the setWasmUrl call.
Where can a Rive animation run: popup, side panel or content script?
Extension pages are the easy case: popup, side panel, options and new tab pages all run under your extension's CSP, so the setup above works as-is.
- Popup: Chrome sizes it to its content, between 25×25 and 800×600 pixels. Give the canvas explicit CSS width and height, or the popup collapses around an empty canvas.
- Side panel and options: the same code; resize the drawing surface when the panel width changes.
- On web pages: rather than running the runtime inside a content script on someone else's page, inject an iframe that points to a page in your extension. List that page in
web_accessible_resourcesso websites can frame it, and the animation runs on your extension's origin under your CSP, isolated from the site's scripts and styles.
"web_accessible_resources": [
{
"resources": ["mascot-frame.html"],
"matches": ["https://*.example.com/*"]
}
]
Use the narrowest matches list you can; it controls which sites may load the resource.
How do I keep a new-tab mascot fast?
A new tab override (chrome_url_overrides) loads every time someone opens a tab, so small costs add up.
- Preload the WASM. Rive's preloading guide uses
<link rel="preload" as="fetch" crossorigin>; the preload URL must match thesetWasmUrl()URL exactly, or the file downloads twice. A relativevendor/rive.wasmon an extension page resolves to the same URL aschrome.runtime.getURL("vendor/rive.wasm"). - Pause when hidden. The sample pauses on
visibilitychange, so background tabs don't keep animating. - One instance. Use a single character with moods, not several canvases.
- Respect reduced motion. Autoplay is off when the user asks for less motion; show the first frame or a still image instead.
Rive also recommends @rive-app/webgl2 for most web projects; the same RuntimeLoader calls apply, but copy the WASM files from that package instead.
Does the same setup work in Firefox?
Yes, with the same rules. MDN's manifest reference says that in Manifest V3 the only values allowed in script-src for extension pages are 'self', 'none' and 'wasm-unsafe-eval', and that "Manifest V3 does not allow remote URLs in script-src." It also notes that Firefox's add-on policies don't allow remote scripts. The bundled WASM, the local .riv and the same CSP string carry over.
For Manifest V2 add-ons, MDN says Firefox can currently run WebAssembly without 'wasm-unsafe-eval' for backward compatibility, but that "this behavior isn't guaranteed," and recommends declaring it.
Do Electron and Tauri apps need the same fix?
The principle is identical: ship rive.wasm with the app, call RuntimeLoader.setWasmUrl() with a local URL, and allow WebAssembly in your CSP.
- Electron. Electron's security checklist warns against loading and executing remote code and recommends serving local pages from a custom protocol instead of
file://. PointsetWasmUrlat a URL on that protocol. If you set a CSP (with a meta tag for local pages), include'wasm-unsafe-eval'inscript-src. - Tauri. Tauri's CSP is only enforced if you set it in the config; when you do, its docs say to "include
'wasm-unsafe-eval'as ascript-src," and they advise against CDN scripts. The runtime downloads the WASM and the .riv withfetch, so make sureconnect-src(ordefault-src) allows the origin you serve them from.
What changes for a Charmotion .riv in an extension?
Nothing in the file. Charmotion's .riv (17 animations from one image, with a Character view model: a mood number from 0 to 9 and success, error, yes, no, enter and exit triggers) is self-contained, so it works offline from the package. The web code that comes with it loads the runtime from a CDN; for an extension, replace that script tag with the bundled import and the two RuntimeLoader calls shown above. The .riv is part of every paid plan (from $20/month).

How to run a Rive animation in a Manifest V3 Chrome extension
- Install the runtime. Install an exact version of @rive-app/canvas and bundle your popup script.
- Copy the WASM. Copy rive.wasm and rive_fallback.wasm from node_modules into the extension folder.
- Point the runtime at them. Call RuntimeLoader.setWasmUrl and setWasmFallbackUrl with chrome.runtime.getURL before new Rive().
- Bundle the .riv. Put the .riv in the package and load it with chrome.runtime.getURL.
- Set the CSP. Add script-src 'self' 'wasm-unsafe-eval'; object-src 'self'; under content_security_policy.extension_pages.
- Check the network. Open the popup's DevTools and confirm no request goes to unpkg or jsDelivr.
Frequently asked questions
Can my extension download .riv files from a server?
Chrome's remote code rules target executed code, and its guide says data such as JSON or CSS is not remote code. A .riv is mostly data, but files that use Rive scripting contain code, and a bundled file works offline and avoids review questions. Bundle it unless you truly need remote content updates.
Why is my Rive popup tiny or empty?
Chrome sizes the popup to its content, between 25×25 and 800×600 pixels. A canvas without CSS dimensions gives it little to measure. Set an explicit width and height on the canvas, then call resizeDrawingSurfaceToCanvas() in onLoad so the drawing buffer matches.
Do I need web_accessible_resources for rive.wasm?
Not for extension pages such as the popup or side panel, which load their own packaged files. You need web_accessible_resources when a website must load something from your extension, such as an extension page injected as an iframe into a site.
Quick answers from this guide
Sources
- Deal with remote hosted code violations, Chrome for Developers
- Manifest: content_security_policy, Chrome for Developers
- chrome.action (popup size limits), Chrome for Developers
- Manifest: web_accessible_resources, Chrome for Developers
- Override Chrome pages, Chrome for Developers
- Preloading WASM, Rive
- @rive-app/canvas package, npm
- content_security_policy (WebExtensions manifest), MDN Web Docs
- Security, Electron
- Content Security Policy (CSP), Tauri