Loading Custom Fonts in Remotion: Self-Hosting and Flicker Fixes
By RenderComp Team Editorial policy
Loading Custom Fonts in Remotion: Google Fonts, Self-Hosted Files, and Flicker Fixes
“My font isn’t applying.” “The first second of the video renders in the wrong typeface, then it flickers into the right one.” If you have shipped a few Remotion projects, you have probably hit one of these. Font problems are among the most common Remotion issues precisely because font loading works differently in a video renderer than in a normal web page.
This guide covers every font loading path in Remotion 4.x: the type-safe @remotion/google-fonts package, self-hosted files with @remotion/fonts, and the manual delayRender() pattern, plus troubleshooting guidance for when fonts flicker, glyphs go missing, or a Lambda render looks different from your local preview.
Why Font Loading Works Differently in Deterministic Frame Rendering
On a regular website, a late-loading font is a cosmetic annoyance: the browser paints fallback text first, then swaps in the webfont a moment later. Users barely notice.
Remotion cannot afford that. Each frame of your video is a screenshot of a headless Chromium page at a specific frame number. If the custom font has not finished loading when frame 0 is captured, the fallback font is baked into the video file permanently. There is no later swap; the pixels are already encoded.
This produces the two classic failure modes:
- The fallback flash. The font finishes loading partway through the render, so the first N frames show the fallback typeface before the video visibly “pops” into the correct font. In parallelized renders the flash can even appear mid-video, because each chunk starts its own browser instance.
- The silent wrong font. The font never loads (bad path, wrong family name, network failure), and the entire video quietly renders in whatever Chromium falls back to. No error, just a deliverable that looks off.
The fix for both failure modes is the same concept: block frame capture until every font is ready. Remotion exposes this through delayRender() / continueRender(), and both official font packages call these for you under the hood. Your job is mostly to use the right package and avoid patterns that bypass the blocking, which matters especially for text-heavy work like kinetic typography or text reveal animations, where the typeface is the video.
Type-Safe Loading with @remotion/google-fonts
The @remotion/google-fonts package gives you a typed import per font family. Install it with:
npx remotion add @remotion/google-fonts
Then load a family and use the returned fontFamily string:
import { loadFont } from '@remotion/google-fonts/Inter';
const { fontFamily } = loadFont('normal', {
weights: ['400', '700'],
subsets: ['latin'],
});
export const Headline: React.FC<{ text: string }> = ({ text }) => {
return (
<div style={{ fontFamily, fontSize: 90, fontWeight: 700 }}>
{text}
</div>
);
};
Three things to know:
loadFont()blocks the render automatically. It registers adelayRender()handle internally, so no frame is captured before the font is available. The function also returns awaitUntilDone()promise if you need to await it explicitly.- Always pass
weightsandsubsets. CallingloadFont()with no arguments downloads every style, weight, and subset of the family, generating a lot of network requests and a common cause of render timeouts. - Font files are fetched over the network at render time. Every fresh browser instance (every local render, every CI run, every Lambda chunk) downloads the files again from Google’s servers.
That last point is why @remotion/google-fonts is the convenience option rather than the production default. A render pipeline that depends on a third-party font server has an external point of failure: no offline renders, no fully reproducible builds, and a single firewall rule or outage stands between you and a finished video. For anything you render repeatedly or sell, self-hosting is more robust, and it is barely more work.
Self-Hosting Fonts with @remotion/fonts and loadFont
Available since Remotion 4.0.164, the @remotion/fonts package loads font files that live in your own project. Install it:
npx remotion add @remotion/fonts
Put your .woff2 files in the public/ folder and reference them with staticFile(). A clean pattern is a dedicated fonts.ts module that loads everything the project uses:
// src/fonts.ts
import { loadFont } from '@remotion/fonts';
import { staticFile } from 'remotion';
export const brandFont = 'Inter';
export const fontsReady = Promise.all([
loadFont({
family: brandFont,
url: staticFile('fonts/Inter-Regular.woff2'),
weight: '400',
style: 'normal',
}),
loadFont({
family: brandFont,
url: staticFile('fonts/Inter-Bold.woff2'),
weight: '700',
style: 'normal',
}),
]);
Like the Google Fonts package, loadFont() from @remotion/fonts handles delayRender() internally, so you do not need to wire up blocking yourself. Import the module for its side effect and the fonts are guaranteed ready before frame capture:
import {
AbsoluteFill,
Sequence,
interpolate,
spring,
useCurrentFrame,
useVideoConfig,
} from 'remotion';
import { brandFont } from './fonts'; // importing kicks off loading before frame 0
const FALLBACK = '-apple-system, "Segoe UI", Roboto, sans-serif';
const AnimatedLine: React.FC<{ text: string }> = ({ text }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const enter = spring({
frame,
fps,
config: { mass: 0.6, stiffness: 160, damping: 14 },
});
const translateY = interpolate(enter, [0, 1], [40, 0]);
const opacity = interpolate(enter, [0, 1], [0, 1], {
extrapolateRight: 'clamp',
});
return (
<div
style={{
fontFamily: `${brandFont}, ${FALLBACK}`,
fontSize: 84,
fontWeight: 700,
color: '#ffffff',
transform: `translateY(${translateY}px)`,
opacity,
}}
>
{text}
</div>
);
};
export const TitleCard: React.FC = () => {
return (
<AbsoluteFill
style={{
background: '#0a0a0a',
justifyContent: 'center',
alignItems: 'center',
}}
>
<Sequence layout="none">
<AnimatedLine text="Self-hosted fonts" />
</Sequence>
<Sequence from={12} layout="none">
<AnimatedLine text="ready before frame one" />
</Sequence>
</AbsoluteFill>
);
};
Note the fallback stack after the custom family. If the font ever fails to load, a system stack like -apple-system, "Segoe UI", Roboto, sans-serif degrades gracefully instead of dropping to an arbitrary serif. For Japanese text, use a stack like "Yu Gothic", "Hiragino Kaku Gothic ProN", "Noto Sans JP", sans-serif.
Why .woff2? It is the smallest widely-supported format and Remotion’s Chromium handles it natively. If you only have a .ttf or .otf, convert it. Smaller files mean faster render startup, especially on Lambda where each chunk loads fonts independently.
Blocking Frame Capture Until Fonts Are Ready
Both packages above call delayRender() for you, but understanding the primitive helps when you need custom behavior: a font fetched from your own asset server, conditional loading based on props, or explicit error handling. The manual pattern uses the web-native FontFace API:
import { cancelRender, continueRender, delayRender, staticFile } from 'remotion';
const handle = delayRender('Loading BrandSans');
const brandSans = new FontFace(
'BrandSans',
`url('${staticFile('fonts/BrandSans-Regular.woff2')}') format('woff2')`,
);
brandSans
.load()
.then(() => {
document.fonts.add(brandSans);
continueRender(handle);
})
.catch((err) => {
cancelRender(err); // fail the render loudly instead of shipping the wrong font
});
A few rules govern delayRender(). Every handle must be resolved — call continueRender(handle) on success or cancelRender(err) on failure; an unresolved handle fails the render with a timeout. The default is 30 seconds, and if a large font on a slow connection needs longer, raise it per call with delayRender('Loading font', { timeoutInMilliseconds: 60000 }), or globally with the --timeout render flag. Label your handles, because the string appears in timeout error messages and turns “a delayRender was not resolved” into a message that names the actual culprit. Finally, run loading at module scope rather than inside useEffect. A useEffect without a delayRender() handle executes after the first frames may already have been captured. This is the single most common cause of the fallback flash, and the fix is module-scope loading.
If you use @remotion/google-fonts and need to sequence logic after the font is ready (measuring text, for example), use the returned waitUntilDone() promise rather than reinventing the blocking yourself.
Weights, Subsets, and Variable Fonts
Load every weight you actually use, and nothing more. With static fonts, each weight is a separate file. If you set fontWeight: 700 but only loaded the 400 file, Chromium will synthesize a faux bold by smearing the regular outlines. The result renders, but it looks subtly wrong and will not match design mockups made with the real bold cut.
Subsets control which characters are included. Specifying subsets: ['latin'] keeps downloads small but means accented characters from latin-ext, Cyrillic, or CJK will render as fallback glyphs or tofu boxes. Match subsets to the languages your video actually displays, including any dynamic data that may arrive with unexpected characters.
Variable fonts collapse the weight problem into one file. A single variable .woff2 covers a whole weight range, declared with a FontFace weight range descriptor:
const interVariable = new FontFace(
'InterVariable',
`url('${staticFile('fonts/InterVariable.woff2')}') format('woff2')`,
{ weight: '100 900' },
);
After document.fonts.add(), any fontWeight from 100 to 900 uses real interpolated outlines. You can even animate fontWeight frame-by-frame with interpolate() for weight-morphing text effects, a technique that static fonts simply cannot support.
CJK Font File Size and Subsetting
CJK fonts are a different weight class. A single weight of a full Japanese font carries thousands of glyphs and typically weighs several megabytes even as woff2. Three strategies, in order of preference:
1. Subset to the text you render. If your composition’s text is known (or comes from a bounded data set), subset the font to exactly those glyphs with pyftsubset from fonttools:
pip install fonttools brotli
pyftsubset NotoSansJP-Regular.ttf \
--text-file=all-strings-used.txt \
--flavor=woff2 \
--output-file=public/fonts/NotoSansJP-subset.woff2
A subset covering a script’s actual text is often two orders of magnitude smaller than the full font. For data-driven pipelines, regenerate the subset from your data source as a build step.
2. Subset to a character-set standard. When text is dynamic and unbounded (user input, API data), subset to a standard coverage level. Kana plus the jōyō kanji covers the vast majority of modern Japanese text; ship that as your main file and accept rare-glyph fallback for edge cases.
3. Ship the full font and raise the timeout. This approach is simplest and heaviest. Acceptable for local rendering, but on distributed renders every chunk pays the load cost, so combine it with a longer delayRender timeout.
A note on @remotion/google-fonts with CJK families: Google Fonts splits CJK fonts into dozens of unicode-range slices so browsers only fetch what they need. In Remotion this can trigger a “too many network requests” warning. The ignoreTooManyRequestsWarning option exists for exactly this case, but the warning is also a signal that a self-hosted subset would serve you better.
Troubleshooting Fallback Flashes, Missing Glyphs, and Lambda Differences
The first frames show the wrong font, then it flickers.
Font loading is not blocking the render. The usual suspects are loading inside useEffect without a delayRender() handle, or a hand-rolled @font-face in CSS with no blocking at all. Move loading to module scope via @remotion/fonts or the manual delayRender pattern above.
The whole video renders in a fallback font, no errors.
Check three things in order. First, the family name: the string in your style={{ fontFamily }} must exactly match the family you passed to loadFont(). Second, the file path: a typo in the staticFile() path yields a 404 that FontFace.load() may surface only as a rejected promise, which is why the catch-and-cancelRender pattern beats silently continuing. Third, confirm the loading module is actually imported by the composition being rendered.
Some characters render as boxes (tofu) or in a different typeface.
The loaded subset does not contain those glyphs. This is common with subsets: ['latin'] when names or data contain accented or CJK characters. Load the additional subsets, or subset your self-hosted file with wider coverage to address it.
It looks right locally but wrong on Lambda. Your machine resolves fonts your render environment does not have. A font installed system-wide renders correctly even if your code never loads it explicitly. The Lambda runtime ships only a minimal font set (Noto Sans in a few weights, Noto Color Emoji, and a handful of script-specific Noto fonts), so anything else must be loaded explicitly by your code. Never rely on system fonts existing anywhere but your own machine. See our Remotion Lambda rendering guide for more environment differences.
Renders fail with a delayRender timeout.
A font file too large for the 30-second default (common with unsubsetted CJK fonts) or a dead network path. Subset the font, self-host it so it loads from the bundle rather than a remote server, and raise timeoutInMilliseconds only as a last resort.
Font Licensing When You Ship or Sell Templates
Loading a font is a technical problem; distributing one is a legal one. If you sell Remotion templates or hand source code to clients, the font files in public/fonts/ travel with the package.
Fonts under open licenses (SIL OFL and similar) may be bundled and redistributed inside a larger work, commercial or not. Inter and the Noto families fall here. Keep the license text alongside the font file, and note that the OFL forbids selling the font files on their own.
Commercial fonts are a different matter. A standard desktop license almost never includes the right to redistribute the font file to your buyers. If a template’s design depends on a commercial typeface, ship the template without the file: declare the fontFamily with a clear system fallback stack, document where buyers can license the font, and let them drop their own copy into public/fonts/.
Fonts served by hosted font platforms are licensed for serving, not extraction. Pulling the woff2 out of the network tab and committing it to your repo violates that license. The practical default for template authors is to build on OFL-licensed fonts, self-host them, and include the license file in the repo; this approach gives buyers immediate clarity and leaves no legal ambiguity on your end.
Summary
- Remotion captures frames deterministically, so fonts must be loaded before frame capture — otherwise the fallback font is baked into your video.
@remotion/google-fontsis the convenient path, but it fetches from remote servers on every render. Constrain it withweightsandsubsetsif you use it.- Self-hosting with
@remotion/fontsandstaticFile()is the production-grade setup: no network dependency, reproducible renders, and it works offline and on Lambda. - Both packages block rendering automatically. For custom loading, pair
FontFacewithdelayRender()/continueRender()/cancelRender(). - Load real weight files (or a variable font) instead of letting Chromium fake bold; match subsets to your actual text, and aggressively subset CJK fonts.
- Never rely on system fonts beyond your own machine, and check redistribution rights before committing font files to a template you distribute.
Skip the setup entirely with RenderComp →
RenderComp’s text-heavy Remotion templates (kinetic title cards, lower thirds, and caption systems) ship with working font loading wired in and editable TypeScript source, so you can swap in your own self-hosted typeface by changing one module.
Frequently asked questions
Do I need to call delayRender() myself when using @remotion/google-fonts or @remotion/fonts?
No. Both packages register and resolve `delayRender()` handles internally. You only need manual `delayRender()` when loading fonts through your own mechanism, such as the raw `FontFace` API or a custom fetch.
Can I just use a font that is installed on my computer?
It will work in local preview and local renders — and break the moment the project renders anywhere else, including Lambda and teammates' machines. Always load fonts explicitly from files in your project.
What format should my font files be?
`woff2`, whenever possible. It is the smallest common format and fully supported by Remotion's Chromium. Convert `.ttf`/`.otf` sources with fonttools (`--flavor=woff2`).
Where should loadFont() be called — component or module scope?
Module scope. Loading at the top level of an imported file starts before any frame is captured. Calling it inside a component body on every render is wasteful, and inside `useEffect` it is actively dangerous because the effect can run after early frames were captured.
Does font loading hurt render determinism?
No — that is the point of the blocking. Once fonts are loaded before frame capture begins, every frame renders with identical font data, and re-renders produce identical pixels.
How many fonts is too many?
Each family, weight, and subset adds startup time to every browser instance your render spins up. Two families with two or three weights each is a healthy ceiling for most compositions; beyond that, consider a variable font.
Now available
Get 1,000+ Remotion Templates
Pay once — no subscription. Lifetime updates. TypeScript-first.
View pricing →