Rendering a Custom Font Dynamically with React + TypeScript

This past week at GIPHY I was working on an interesting feature for one our creation tools. The tool needed to allow the user to upload a font file and then render a preview of that font. Various Google and StackOverflow searches led me down a number of paths but they either didn't match what I needed or they weren't compatible with TypeScript. I decided to write this post after finally putting together a solution I was satisfied with.

This post will demonstrate and explain how to upload a font file to a webpage and render it dynamically using React and TypeScript.

An example of this functionality in action can be seen in the CodePen below:

A CodePen example.

The Code

This section examines each part of the code more closely. It's nothing too crazy, but there are some interesting lines worth mentioning.

One important note is that this solution uses the FontFace API which is still experimental and as such is subject to change and/or experience browser compatibility issues.

The Functional Component

const App = () => {
  return (
    <div className="container">
      <input type="file" onInput={onFileInput} accept={".ttf"} />
      <span className="text" style={{ fontFamily: "uploadedFont" }}>
        Sample Text
      </span>
    </div>
  );
};

This is the actual component that's being rendered.
- The accept={".ttf"} attribute on the file input specifies a filter for  what file types the user can pick from the file input dialog box. Keep in mind, this attribute should not be used as a validation tool. File uploads  should be validated on the server. (Source)
- The style={{ fontFamily: "uploadedFont" }} attribute on the span won't have any effect on the sample text until uploadedFont is actually set in the onFileInput helper function (explained below).

convertFontFiletoBase64 Helper Function

This function encodes a File object using base64 and returns the resulting string.

const convertFontFiletoBase64 = async (file: File) => {
  const result_base64 = await new Promise((resolve) => {
    const fileReader = new FileReader();
    fileReader.onload = () => resolve(fileReader.result);
    fileReader.readAsDataURL(file);
  });
  return result_base64 as string;
};

- This function is asynchronous due to its use of the FileReader class.
- as string is needed on the last line because the return type of fileReader.result is string | ArrayBuffer. Without this cast you may see a Type 'unknown' is not assignable... error elsewhere in the code.

onFileInput Helper Function

This function takes the file that the user uploaded, passes it to the helper function explained above, instantiates a FontFace object with the result, and adds that object to the document so that text elements can use it.

const onFileInput = async (e: SyntheticEvent<HTMLInputElement, Event>) => {
  const { files } = e.currentTarget;
  if (files && files?.length > 0) {
    const font = await convertFontFiletoBase64(files[0]);

    // @ts-ignore
    const fontFace = new FontFace("uploadedFont", `url(${font})`);
    (document as any).fonts.add(fontFace);
    await fontFace.load();
  }
};

- @ts-ignore is used here because FontFace is an experimental API and thus not yet supported by TypeScript. A Cannot find name 'FontFace'. error will show without it.
- as any should generally be avoided but it is needed here for the same reason given above.  
- This function could be a good place for some basic file validation. If the application needed to store the font file on the back-end it would be wise to add more robust validation.
- await is used in the last line since the .load() function returns a Promise. (Source)

Wrapping Up

I really enjoyed hunting down the solution to this problem and I'm proud of what I was able to come up with.

Thanks for reading! If you found this post helpful, give me a shout or a follow on Twitter @brodan_ and stay tuned for more!