How to Configure and Package Fonts with Tamagui and Next.js
A guide to properly configuring and implementing custom fonts in Tamagui and Next.js applications, focusing on performance and cross-platform compatibility.
Problem
I was trying to add a font using Tamagui's font generation tool, but, the correct font wasn't being rendered to the (this) website. It took a good bit of trail and error to figure out what was going on, and how to fix it.
Importing both the correct font files for web, mobile and general configuration is tetious, and error prone and just generally something that most developers don't want to spend time on.
After digging around the generated package a bit, there were a couple of issues that stuck out:
- Only .ttf files were generated and packaged. For web projects .woff2 files are generally the best option, for many reasons.
- There was no .css files generated or rendered with the page.
- There are a couple of hardcoded css variables that are used in the generated package.
- Only the font-family was added to the component without changing the actual font being rendered to the screen.
Using the correct file type
For web using the .woff2 file type is the most performant, and generally the best option. For mobile, using the .otf file type.
Looking into how Tamagui configures fonts
The @tamagui/inter package that is shipped by default was my starting place after trying to configure fonts generated with the tamagui cli tool.
Digging into @tamagui/inter
There were a couple of things that I noticed that were different from the generated package and the @tamagui/inter package:
- Fonts are base64 via subset.ts encoding for the fonts, which is generally not recommended.
- It exported a css file that was imported into the project.
Looking at the Tamagui.dev website source code
cmd + shift + u and searching on .woff2, fonts are being loaded from public/ and served like any regular font.
Base64 encoding
I did a bit of searching and didn't find a compelling reason to use base64 encoding for fonts. It's generally not recommended, and can cause performance issues.
Solution
Add next/font to your _app.tsx file. Add a className and font variable in your root. Pull out a generic createFont function from @tamagui/inter and use it in your project.
_app.tsx
import '@tamagui/core/reset.css'
import { Source_Serif_4, EB_Garamond } from 'next/font/google'
import 'raf/polyfill'
// ... rest of imports
const bodyFont = Source_Serif_4({
subsets: ['latin'],
display: 'swap',
variable: '--my-body-font',
})
const displayFont = EB_Garamond({
subsets: ['latin'],
display: 'swap',
variable: '--my-display-font',
})
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useRootTheme()
return (
<NextThemeProvider onChangeTheme={(next) => setTheme(next as any)}>
<Provider disableRootThemeClass disableInjectCSS defaultTheme={theme}>
<div className={`${bodyFont.variable} ${displayFont.variable}`}>
{children}
</div>
</Provider>
</NextThemeProvider>
)
}
tamagui.config.ts
const createDisplayFont = <A extends GenericFont>(
font: Partial<A> = {},
{
sizeLineHeight = (size) => size,
sizeSize = (size) => size,
}: {
sizeLineHeight?: (fontSize: number) => number
sizeSize?: (size: number) => number
} = {}
): FillInFont<A, keyof typeof defaultSizes> => {
const size = Object.fromEntries(
Object.entries({
...defaultSizes,
...font.size,
}).map(([k, v]) => [k, sizeSize(+v)])
)
return createFont({
family: isWeb
? 'var(--my-display-font), -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif'
: 'Playfair Display',
lineHeight: Object.fromEntries(
Object.entries(size).map(([k, v]) => [k, sizeLineHeight(getVariableValue(v) * LINE_HEIGHT)])
),
weight: {
4: '300',
},
letterSpacing: {
4: 0,
},
...(font as any),
size,
})
}
After implementing these changes, your fonts should be rendering as expected on your Next.js site.