Creating Custom Fonts with React-Native-Skia

Git Stash Apply
4 min readMay 16, 2024

Introduction

When working with custom designs in React Native, creating custom fonts with specific styles can be challenging. In this article, we will explore how to implement a custom font using @shopify/react-native-skia library.

Problem

The design requirements for our custom font are as follows:

Let’s talk a bit — what we need to implement:

  • The font should have a 1px stroke.
  • The font should have a bottom shadow with a width of 4px and a Y translation of 4px.

React-Native-Skia

Skia is a cross-platform 2D graphics library that provides a set of drawing primitives that run on iOS, Android, macOS, Windows, Linux, and the browser. React-native-skia brings this library to react native.

Installation:

  1. Install package:
yarn add @shopify-react-native-skia

2. iOS pod install:

npx pod-install ios

If you have any issues with package installation please check Official Docs

Implementation

We will divide the font rendering into three main components:

  1. Outer Stroke
  2. Inner Font
  3. Outer Shadow

Preparing Skia for the Base Font

We’ll use the Anek-Latin font as our base font. (here is guide how to install custom fonts)

First, define the base font style and create the Skia font object:

import { Skia } from "@shopify/react-native-skia";

export const fontStyle = {
textStyle: {
color: Skia.Color('white'),
fontFamilies: ["Anek-Bold"],
fontSize: 36,
lineHeight: 24,
letterSpacing: -0.8,
},
paragraphStyle: {
foregroundPrimaryStyle: 1,
foregroundSecondaryStyle: 0,
foregroundPrimaryColor: Skia.Color('black'),
foregroundSecondaryColor: Skia.Color('white'),
foregroundStrokeWidth: 4,
},
shadow: {
dx: 0,
dy: 4,
},
};

Note: very important to use Skia.Color for color definition.

Create font manager using “useFont()”

export const CustomFont = () => {
const customFontMgr = useFonts({
["Anek-Bold"]: [require("../../assets/fonts/AnekLatin-Bold.ttf")],
});
...
}

Let’s Rock! Outer stroke.

Create paint object and paragraph primitive using Skia.Paint() and Skia.ParagraphBuilder.Make()

...  
const outerStroke = useMemo(() => {
if (!customFontMgr) {
return null;
}

const foregroundPaint = Skia.Paint();
foregroundPaint.setStyle(fontStyle.paragraphStyle.foregroundPrimaryStyle);
foregroundPaint.setColor(fontStyle.paragraphStyle.foregroundPrimaryColor);
foregroundPaint.setStrokeWidth(
fontStyle.paragraphStyle.foregroundStrokeWidth,
);

return Skia.ParagraphBuilder.Make({
textAlign: TextAlign.Center
}, customFontMgr)
.pushStyle({ ...fontStyle.textStyle, ...style }, foregroundPaint)
.addText(text)
.pop()
.build();
}, [customFontMgr, text]);
...

Let’s speak a bit what we see here:

  1. Skia.Paint() — creates Paint primitive.
  2. Now we can assign any properties for foregroundPaint
  3. Skia.ParagraphBuilderMake() creates Paragraphprimitive.

Let’s render something on the screen:

.... 

return (
<Canvas style={{ width, height }}>
<Paragraph paragraph={outerStroke} x={0} y={0} width={width} />
</Canvas>
);
Rendering outer style

Inner Font

Now, let’s implement the body of the font:

...

const innerFont = useMemo(() => {
if (!customFontMgr) {
return null;
}

const innerFontStyle = {
...fontStyle.textStyle,
...style,
color: Skia.Color(color ?? "white"),
};

return Skia.ParagraphBuilder.Make(
{ textAlign: TextAlign.Center },
customFontMgr,
)
.pushStyle(innerFontStyle)
.addText(text)
.pop()
.build();
}, [customFontMgr, text]);

...

return (
<Canvas style={{ width, height }}>
<Paragraph paragraph={primaryParagraph} x={0} y={0} width={width} />
<Paragraph paragraph={secondaryParagraph} x={0} y={0} width={width} />
</Canvas>
);
Looks better, right?

Outer Shadow

Finally, let’s implement the outer shadow using the Group, Paint, and Shadow components from @shopify/react-native-skia:

import { Group, Paint, Shadow } from '@shopify/react-native-skia';

...
// Component
return (
<Canvas
style={{
width: width,
height: height,
}}
>
<Group
layer={
<Paint>
<Shadow
blur={0}
dx={fontStyle.shadow.dx}
dy={fontStyle.shadow.dy}
color={colors.black}
/>
</Paint>
}
>
<Paragraph paragraph={outerStroke} x={0} y={0} width={width} />
<Paragraph paragraph={innerFont} x={0} y={0} width={width} />
</Group>
</Canvas>
);

<h3>WHAT’S IN THE BOOOOOOOOX ?!!!</h3>

Dynamic Canvas Size

Maybe you saw that <Canvas /> component contains { width: width, height: height} parameters.

We want our canvas to adjust its size dynamically based on the content. React-Native-Skia provides the measureText helper:

const font = useFont(require('../assets/fonts/AnekLatin-Bold.ttf'), textStyle.fontSize);
const { width, height } = font?.measureText(text);

Let’s create some hook:

// useFontMeasurements.ts

import { DataSourceParam, useFont } from "@shopify/react-native-skia";
import { useEffect, useState } from "react";

type Props = {
text: string;
size: number;
fontSource: DataSourceParam;
};

export const useFontMeasurements = ({ text, size, fontSource }: Props) => {
const [textParams, setTextParams] = useState<{
height: number;
width: number;
}>({ height: 0, width: 0 });
const font = useFont(fontSource, size);

useEffect(() => {
if (!fontSource || !text || !size || !font) return;

const { width, height, y } = font.measureText(text);

setTextParams({ width, height: height - y / 2 });
}, [font, size, fontSource, text]);

return {
height: textParams?.height,
width: textParams?.width,
};
};

Now we can use it in our <CustomFont />component:

...

const { height, width } = useFontMeasurements({
text: text,
fontSource: require("../../assets/fonts/AnekLatin-Bold.ttf"),
size: style?.fontSize ?? fontStyle.textStyle.fontSize,
});

...

You can find source code here:

https://github.com/gitstashapply/medium-custom-fonts

Please let me know if you found this article useful and if you’re interested in more articles like this.

Cheers!

--

--