Every Google Fonts family works with no setup: a box loads the faces its text uses when it
appears, and setFontFamily("Pacifico") loads Pacifico before applying it. Your own fonts, and fonts your users upload,
take one setting: a lookup that tells the library where a family's files are.
How loading works
- A box shows its own words faintly in a stand-in font while its fonts download, then draws them in the real font. It never shows a blank box or a spinner.
- Each face is measured from the font file itself, so line breaks are the same in every browser and in print.
- Along with the faces a box uses, the library loads each family's regular, bold, italic and bold italic faces, so Bold and Italic in your toolbar draw the real face at once.
- A face that fails to download is retried automatically. Until it arrives, its text draws in a stand-in font and the box shows a warning mark, described below.
Change the font
// Konva
await rt.manager.setFontFamily("Playfair Display");
// Plain canvas
await rt.setFontFamily("Playfair Display");
setFontFamily loads the family first, then applies it, and the promise resolves with the text already laid out in
the new font. Until then the box keeps its current font. Bold, italic and weight are kept. Like every formatting
command, it changes the selected text while a box is being edited, or the whole box when it is only selected, or the
boxes you pass: setFontFamily("Oswald", [title]).
Your own fonts
Tell the library where a family lives with a lookup. The lookup is asked each time a family is met, so fonts your app
learns about later, such as an upload, are found without calling configureRichText again.
import { configureRichText } from "@mycreativeshop/rich-text"; // on Konva: "@mycreativeshop/rich-text/konva"
configureRichText({
fonts: {
lookup: (family) => {
if (family === "Brand Serif") {
return { files: [{ weight: 400, url: "/fonts/brand.woff2" }, { weight: 700, url: "/fonts/brand-bold.woff2" }] };
}
if (family === "Acme Sans") return { fontFaceUrl: "https://cdn.example.com/acme-sans.css" };
return undefined; // a Google family, or one you know nothing about
},
},
});
The lookup returns a FontInfo for a family, or nothing:
| Field | Is |
|---|---|
files |
The family's font files, one per face: { weight, style, url } or { weight, style, bytes }. Any of .woff2, .woff, .ttf, .otf. Used before fontFaceUrl. |
fontFaceUrl |
A stylesheet with the family's @font-face rules |
weights |
The weights the family ships, so a weight it lacks resolves to one it has |
A file's weight defaults to 400 and its style to "normal". A weight the family lacks is drawn from the nearest face
it has; the library never makes a fake bold. Return nothing for a family you know nothing about. If the lookup throws,
the family is loaded as if there were no lookup.
Note: The library reads font files itself, to measure them. A font on another origin must be served with an
Access-Control-Allow-Originheader that allows your page.
When text uses one of your fonts, its address is recorded in the document (fontMetadata), with the weights it ships
(a stylesheet font's weights; files carry their own). The design then opens anywhere, such as a print server or
another app, without your lookup, and draws the same weights there. Files given as bytes have no address: a design
that uses them names the family only, and opens where your lookup knows it.
Fonts your users upload
Give the lookup the uploaded file, as an object URL or as bytes:
const uploaded = new Map<string, string>(); // family -> object URL
configureRichText({
fonts: { lookup: (family) => (uploaded.has(family) ? { files: [{ url: uploaded.get(family) }] } : undefined) },
});
input.onchange = async () => {
const file = input.files![0];
const family = file.name.replace(/\.[^.]+$/, "");
uploaded.set(family, URL.createObjectURL(file)); // a file with no weight draws at 400
await rt.manager.setFontFamily(family); // plain canvas: rt.setFontFamily(family)
};
An object URL works only in the page that made it, and it is recorded in the document like any address. To keep a design that uses an uploaded font, store the file on your server and have the lookup return that address.
Google Fonts from your own host
fonts.cssUrl returns the stylesheet address for one face. Use it to serve Google families from your own server:
configureRichText({
fonts: {
cssUrl: ({ family, weight, style }) =>
`https://fonts.example.com/css?family=${encodeURIComponent(family)}&weight=${weight}&italic=${style === "italic"}`,
},
});
Return null to fetch nothing for a face. cssUrl is not asked about system families such as Arial, or about fonts a
document records its own address for. The default is Google Fonts.
Load fonts early
loadFonts starts downloads before a box needs them - as soon as a design's JSON arrives, or for a font picker's previews:
import { loadFonts } from "@mycreativeshop/rich-text"; // on Konva: "@mycreativeshop/rich-text/konva"
await loadFonts(doc); // every face the document uses
await loadFonts(["Lato", "Bitter"]); // regular, bold, italic and bold italic
await loadFonts([{ family: "Lato", weights: [300, 900], italic: true }]);
loadFonts never throws. It resolves with { loaded, failed, allSuccess }: the families that can be drawn, and the
ones that could not load. It waits at most 5 seconds; a family still loading then is in neither list, and
allSuccess is true only when every family is in loaded. Every face is fetched once, whether a box or
loadFonts asks first.
Wait for fonts
| Wait | For |
|---|---|
await box.fontsReady() |
One box laid out on its real fonts, including fonts a later edit brought in |
await richTextReady(stage) |
Every box in a Konva stage, layer or group |
await rt.ready() |
Every box on a plain canvas |
Wait before you export an image or measure text (see Vector text). A font that never arrives does not hold a wait forever: a wait gives up on it after 5 seconds, and its text draws in a stand-in font. The font keeps loading, and the box redraws in it when it arrives.
When a font can't load
The text keeps its words in a stand-in font, and the box shows a warning mark in its bottom-right corner. Pointing at the mark shows "Font failed to load. Apply a new font or refresh page". Your app is told why:
configureRichText({
fonts: {
onFontError: (error) => {
report(`${error.family} ${error.weight} ${error.style}: ${error.reason}`, error.url);
},
},
});
onFontError is called when text will look wrong: once per face, and again only if the face recovers and then fails
again. Fonts loaded only ahead of time (a picker's previews, the bold face your toolbar may need) and the fallback fonts
for other languages are not reported.
reason |
Means |
|---|---|
cors-blocked |
The font is on another origin that does not allow cross-origin reads |
css-fetch-failed |
The stylesheet could not be fetched |
no-matching-rule |
The stylesheet has no rule for this face |
font-fetch-failed |
The font file could not be fetched |
font-parse-failed |
The file is not a font the library can read (.woff2, .woff, .ttf, .otf) |
font-draw-failed |
The file loaded, but the browser would not draw it |
transient is true when trying again may fix it, such as a network error; the library retries those by itself. url
is the address that failed, when there is one. onFontError: null removes the callback.
Which fonts a design uses
box.getUsedFonts() lists the families a box uses, and replaceFonts swaps one for another everywhere in it. See
Find and replace fonts.