Assets

Add images, video, audio, fonts, and animations to a paywall by importing files. The build handles optimization, hosting, and caching.

Add media to a paywall by importing files from an assets/ directory. The build handles optimization, hosting, and caching; there is nothing to configure and no upload step.

Where assets live

Assets follow the same two-level pattern as components and messages: shared at the project root, local inside a paywall.

superwall/
├── assets/               shared across every paywall
└── paywalls/pro/
    └── assets/           this paywall's own

Every asset belongs in an assets/ directory — superwall/assets/ for shared files, superwall/paywalls/<id>/assets/ for one paywall's own. If a large asset lives anywhere else, the build fails and names the file.

Use an image

Import the file and use it like any URL:

import hero from "@/assets/hero.jpg";          // shared: superwall/assets/
import badge from "../assets/badge.png";        // this paywall's own

<img src={hero} alt="" />

CSS url() works the same way. Imports typecheck because of the generated superwall.d.ts — one more reason to commit it.

Supported out of the box:

KindFormats
Imagespng jpg jpeg webp avif gif svg ico apng
Videomp4 webm mov m4v
Audiomp3 m4a aac wav ogg
Fontswoff2 woff ttf otf
Animation & 3Dlottie riv glb

?url, ?raw, and ?inline import suffixes work too, as do CSS modules.

How hosting works

You never choose where an asset is served from — the build decides, and nothing about your code changes either way:

  • Video, audio, and fonts are always served from Superwall's CDN, whatever their size. Video streams properly instead of being carried by the paywall, and one upload is reused across every version of every paywall.
  • Images embed in the paywall when small and move to the CDN when large.
import promo from "../assets/promo.mp4";

<video src={promo} autoPlay muted loop playsInline />

Custom fonts

A relative-path @font-face is the whole setup:

@font-face {
  font-family: "Manrope Custom";
  font-display: swap;
  font-weight: 200 800;
  src: url("../assets/manrope-latin.woff2") format("woff2-variations");
}

:root { --sans: "Manrope Custom", ui-sans-serif, system-ui, sans-serif; }

A few habits keep fonts cheap:

  • Subset before you ship. A full variable font carries alphabets the paywall will never render — latin-only Manrope is around 24 kB against roughly 90 kB for the whole family.
  • Ship woff2. Anything older is bytes for nothing you support.
  • Google Fonts go in a CSS @import, at the top of the stylesheet — never React-rendered <link> tags. The stylesheet ships in the page itself, so the browser finds the @import immediately; a rendered <link> waits for JavaScript to run first, and the text flashes.
  • One family plus one mono is a good budget.

The custom-fonts example shows a local file and a Google Fonts import side by side.

Lottie

Two ways to ship a Lottie animation, with different trade-offs:

// 1. Animation JSON — embedded in the paywall. Offline-proof, zero requests.
//    Best for small animations.
import spinner from "@/assets/spinner.json";

// 2. A .lottie file — served from the CDN and pre-cached on device by the
//    SDK before the paywall opens. Best for bigger animations.
import intro from "@/assets/intro.lottie";

Rive

.riv files load like any asset, plus one required setup step:

import { useRive, RuntimeLoader } from "@rive-app/canvas";
import riveWasm from "@rive-app/canvas/rive.wasm?url";
import smiley from "../assets/smiley.riv";

RuntimeLoader.setWasmUrl(riveWasm);
RuntimeLoader.setWasmFallbackUrl(null);

const { RiveComponent } = useRive({ src: smiley, stateMachines: "State Machine 1", autoplay: true });

Rive fetches its WebAssembly engine from a CDN by default, and published paywalls cannot reach external CDNs. Bundle the wasm with the ?url import as above, and null the fallback so a failure stays loud rather than silently retrying a CDN that will never answer.

Also pass the file's real state-machine name — naming one that doesn't exist leaves a blank canvas and no error. The with-rive example is the reference.

Multi-page flows

Nothing to do — while the user is on the current page, the next pages' images, video, and fonts warm automatically. .lottie and .riv files go further: the SDK pre-caches them on device before the paywall even opens.

Keep it light

  • Big imagery is fine — it's served from the CDN and cached, not carried by the paywall itself.
  • Compress and size media for a phone screen; every open pays for what the paywall loads.
  • Pushing files over 50 MB warns — every future clone of the source pays for them — but nothing is capped.

How is this guide?

On this page