To draw a variable font in HTML5 Canvas, load it with the CSS Font Loading API, wait for FontFace.load(), add the loaded face to document.fonts, set ctx.font with CSS font shorthand, and only then call a text-drawing method. A variable font can contain several design axes in one file, but the file—not Canvas—defines which axes and value ranges actually exist.
The reliable Canvas workflow
Canvas does not automatically wait for a web font before painting. If you draw while the font is still unavailable, the browser can use a fallback face and your text may have different widths, line breaks, and visual weight. Use this sequence:
- Create a
FontFacewith a family name and the font URL. - Call
load()and await the returned promise. - Add the resolved face to
document.fonts. - Obtain the 2D context and assign
ctx.font. - Draw text only after those steps complete.
The default Canvas font is 10px sans-serif. The value assigned to ctx.font uses CSS font shorthand, so include a size and a family, such as 600 32px "Example Variable", sans-serif.
Complete HTML and JavaScript example
Place a variable-font file at /fonts/example-variable.woff2. This example waits for the font, scales the backing store for a crisp result, and then draws text.
Do these 3 things before closing this tab:
1Scan for outdated or missing drivers - takes under a minute2Repair Windows errors before they cause bigger problems3Fix the driver behind crashes, sound loss and screen glitches#1 Best Overall
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>Variable font on Canvas</title>
<canvas id="art" width="800" height="180"></canvas>
<script type="module">
const face = new FontFace(
"Example Variable",
'url("/fonts/example-variable.woff2")'
);
async function draw() {
try {
await face.load();
document.fonts.add(face);
const canvas = document.querySelector("#art");
const ratio = window.devicePixelRatio || 1;
const cssWidth = 800;
const cssHeight = 180;
canvas.width = cssWidth * ratio;
canvas.height = cssHeight * ratio;
canvas.style.width = `${cssWidth}px`;
canvas.style.height = `${cssHeight}px`;
const ctx = canvas.getContext("2d");
ctx.scale(ratio, ratio);
ctx.clearRect(0, 0, cssWidth, cssHeight);
ctx.font = '600 32px "Example Variable", sans-serif';
ctx.fillStyle = "#111827";
ctx.fillText("Variable font", 20, 70);
} catch (error) {
console.error("Font could not be loaded; using fallback text.", error);
}
}
draw();
</script>
</html>
The try/catch prevents a network or decoding failure from stopping the rest of the page. In a production application, decide whether a fallback rendering is acceptable or whether you should report the failure and avoid exporting an image with the wrong metrics.
What “variable” means
A variable font packages a range of designs in one font file. Common registered axes include weight (wght), width (wdth), italic (ital), slant (slnt), and optical sizing (opsz). A particular file may omit any of these and may add custom axes. Always inspect the font documentation or metadata for the exact tags, minimums, maximums, and defaults.
Axis tags are four ASCII characters and are case-sensitive. Registered tags are conventionally lowercase; custom tags are conventionally uppercase. A value outside the font’s declared range is not a portable way to request a design, so clamp values to the range supplied by the foundry.
Choosing an axis control
| Design need | Preferred control | Canvas qualification |
|---|---|---|
| Weight | CSS font-weight expressed in the ctx.font shorthand |
Use a value supported by the font; the file determines the useful range. |
| Width | CSS font-stretch or the Canvas fontStretch property where supported |
The documented Canvas property uses keyword values; its documentation does not support percentage values. Do not treat it as arbitrary numeric wdth control. |
| Italic or oblique | CSS font-style in the font shorthand |
Use the style that the font exposes; an ital axis is not guaranteed. |
| Optical size or a custom axis | CSS font-variation-settings when rendering CSS text |
The reviewed Canvas interfaces do not establish a portable, direct arbitrary-axis property on a 2D context. Test every target browser and font before depending on one. |
For standard axes, prefer the high-level property because it communicates intent and lets the browser apply the registered axis semantics. CSS’s font-variation-settings is the low-level mechanism for custom axes or cases where no suitable high-level property exists, but that CSS declaration controls CSS text. It does not, by itself, prove that a Canvas context accepts the same arbitrary axis settings.
The Tool Desk
Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →Do not build production code around ctx.fontVariationSettings as though it were a universally available Canvas API. The documented Canvas context exposes separate font controls, including fontStretch, while cross-browser arbitrary-axis behavior is not established by the references used here. If custom-axis control is essential, keep a browser test matrix or render that text outside Canvas.
Using registered properties in a Canvas font string
Registered weight and style values can be put directly in the CSS shorthand:
ctx.font = '750 28px "Example Variable", sans-serif';
ctx.fillText("Heavy text", 20, 45);
ctx.font = 'italic 28px "Example Variable", sans-serif';
ctx.fillText("Italic text", 20, 90);
Whether a numeric weight or italic style produces a continuous variation depends on the font’s own axis definitions. A static-looking result can simply mean that the file has a limited range or no matching axis.
CSS feature detection around Canvas
When the same variable font is used for ordinary HTML text, a feature query can gate CSS variation settings:
Free tools Windows power users keep installed
One-click scans. No signup required.
Rank #3
@supports (font-variation-settings: "wdth" 115) {
.condensed-label {
font-variation-settings: "wdth" 115;
}
}
This tells you whether the browser accepts that CSS declaration. It does not establish arbitrary-axis support through Canvas. Keep Canvas compatibility decisions separate and verify the exact browser, operating-system font stack, and file you ship.
Font loading, caching, and redraw behavior
Load once and reuse the face
Create and load a FontFace once per family and retain it when drawing many frames or canvases. Repeatedly constructing faces adds needless work and can make timing harder to reason about. The browser may cache the underlying resource, but your code should still await the face before the first draw.
Redraw after a late load
If your application initially paints a fallback and later receives the variable font, redraw the canvas after the face is added. Canvas pixels do not automatically change when a font finishes loading. Recompute measured widths, wrapping, alignment, and any hit-test coordinates during that redraw.
Handle a failed load explicitly
Common causes include a wrong URL, a server response that is not a font, a blocked cross-origin request, an unsupported format, or a corrupted file. Log the error, show a known fallback, and avoid silently exporting a result whose typography does not match the design.
Rank #4
Text measurement and layout details
Set ctx.font before calling measureText(); measurements belong to the current font state. If you change weight, style, size, or family, measure again. Variable-axis changes can alter advance widths and therefore line wrapping, centering, clipping, and collision calculations. For deterministic output, keep the font-loading promise in the same initialization path that creates the canvas scene.
Canvas text is drawn at a baseline. Use textBaseline, textAlign, and explicit coordinates rather than assuming that changing a font axis leaves the visual bounds unchanged. For multi-line text, calculate each line after the final font state is selected.
Performance and reliability checklist
- Prefer WOFF2 when your supported browsers and licensing permit it, and serve it with a correct font media type.
- Load the face before starting an animation or export job so a fallback frame is not captured as the final output.
- Reuse a loaded face and avoid changing font state for every glyph when batching is possible.
- Measure only after setting the exact font string used for drawing.
- Use a device-pixel-ratio scale for sharp output, but remember that larger backing stores consume more memory and drawing time.
- Test slow-network, offline, blocked-font, and corrupted-file paths; a successful local development load does not cover them.
- Verify glyph coverage. A variable font can load successfully while particular characters still fall back to another font.
- Record the browser and font version in visual-regression tests because browser support and Baseline labels can change.
Troubleshooting common failures
The text looks like a system font
Confirm that await face.load() completed, that document.fonts.add(face) ran, and that the family name in ctx.font exactly matches the name passed to FontFace. Check the network panel for a 200 response and verify that the response is the intended font file.
Changing wght has no visible effect
The file may not contain a weight axis, the requested value may be outside its range, or the browser may be using a fallback glyph. Check the font’s axis metadata and test with a clearly different registered weight before investigating Canvas behavior.
Best Value
- Used Book in Good Condition
Width percentages do not work with fontStretch
The documented Canvas fontStretch control uses keyword values and does not support percentage values. Use a supported keyword where it expresses the design, or treat arbitrary numeric width as a compatibility-sensitive requirement rather than assuming it is available.
The first export has different line breaks
The export probably ran before the font promise resolved. Gate export behind the same initialization promise used for drawing, then clear and redraw the canvas before serializing it.
Only some characters look wrong
Loading succeeded, but the requested glyphs may not be present. Check the font’s language and Unicode coverage and provide an intentional fallback stack in the font shorthand.
A CSS test passes but Canvas still cannot vary an axis
@supports tests CSS parsing, not a direct arbitrary-axis Canvas API. Keep the CSS and Canvas tests separate and use a real rendering test for each browser you support.
Windows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallCrashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minuteOr skip the browser setup
If your goal is to capture a page that already renders variable-font text, ScreenshotNeo can return a screenshot or PDF from one request. It accepts the consent banner like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each cleanup step can be disabled. Only clean shots are billed: bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and the response identifies the result with X-Page-Verdict and X-Billed headers. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.
Use the API documentation at https://screenshotneo.com/docs/ for the complete option list, including full-page capture, lazy-image loading, CSS-selector element capture, dark mode, device presets, custom viewports, retina scale, PDF paper and margin controls, custom CSS and JavaScript, click and wait actions, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, TTL-based caching, signed image links, asynchronous webhooks, bulk capture of up to 100 URLs per call, usage data, and the OpenAPI specification. Parameter names used by other screenshot APIs are accepted to ease migration.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python
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)
Node.js
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
The Free plan includes 1,000 shots per month without a card; paid plans start at $5 for 3,000 shots, and yearly billing gives two months free. Create a free ScreenshotNeo account to try the capture without adding a card.
The Bottom Line
Load the variable font with FontFace, await it, add it to document.fonts, set ctx.font, and draw afterward. Use high-level registered font properties when possible; treat arbitrary custom-axis control on Canvas as browser- and font-dependent until your own tests prove otherwise.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




