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!

--

--