Left Arrow.Back
Left Arrow.Back

Notes / CSS / Typography Setup

Typography Setup

A word annotated with typography metrics.

Setting up typography in Next.js using vanilla-extract & Capsize

Last Tended

Status: seed

Letters in the browser render white space above & below them. From a design perspective however, this vertical space should be reduced to the height of a capital letter. Capsize fixes this. Trimming the empty space. This note details how to set up fonts & Capsize in a Next.js project.

The letter 'N' in the middle of a circle.

1. Start a Next.js Project

In the terminal, run:

> npx [email protected] --typescript_
2 people working on a frame of a sci-fi fighter jet in a docking bay.

2. Remove Unrequired Boilerplate


Delete the following directories & files highlighted in red as they won't be required:

my-next-app  ├── pages   
├── api
├── _app.ts
└── index.tsx
├── public
├── favicon.ico
└── vercel.svg
├── styles ...


Replace the contents of the following files:


import type { AppProps } from "next/app";
export default function MyApp({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />;


import type { NextPage } from "next";
const Home: NextPage = () => {
return <div>homePlaceholder</div>;
export default Home;
The words 'Google Fonts'.

3. Add Font Files

For this example, we will only be using 1 font family, Inter. Download the family & move the variable font file Inter-VariableFont_slnt,wght.ttf into the directory /public/fonts/.


4. Install vanilla-extract

For this example, we'll be using vanilla-extract to generate our CSS. In the terminal, run:

> npm install @vanilla-extract/css @vanilla-extract/next-plugin_

Replace the /next.config.js file with the following:


const { createVanillaExtractPlugin } = require("@vanilla-extract/next-plugin");
const withVanillaExtract = createVanillaExtractPlugin();
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
module.exports = withVanillaExtract(nextConfig);
A canoe with oar.

5. Install Capsize

Capsize is a library that trims unwanted space above & below typography that is applied by the browser. In the terminal, run:

> npm install @capsizecss/core @capsizecss/vanilla-extract @capsizecss/metrics_
'Times New Roman' written repeatedly in different font styles.

6. Create Typography Styles

Create a file /styles/typography.css.ts. This file will hold all typographic information & define all font styles. Add the following:


import { FontMetrics } from "@capsizecss/core";
import interFontMetrics from "@capsizecss/metrics/inter";
import { createTextStyle } from "@capsizecss/vanilla-extract";
import { style } from "@vanilla-extract/css";
interface Meta {
fallback: string;
files: {
variable?: string;
normal?: string;
bold?: string;
format: string;
metrics: FontMetrics;
name: string;
weights: {
variable?: string;
normal?: number;
bold?: number;
type FontFamilyId = "INTER";
type Fonts = Record<FontFamilyId, Meta>;
const FONT_DIR = `/fonts`;
export const fonts: Fonts = {
fallback: `sans-serif`,
files: {
variable: `${FONT_DIR}/Inter-VariableFont_slnt,wght.ttf`,
format: `truetype-variations`,
metrics: interFontMetrics,
name: `Inter`,
weights: {
variable: `100 900`,
normal: 400,
bold: 700,
// https://type-scale.com
// Major Third
// base: 16
const typeScale = {
s: 12.8,
m: 16,
l: 20,
xl: 25,
interface Props {
id: FontFamilyId;
leading: number;
size: number;
function calcFontCss({ id, leading, size }: Props) {
return style([
fontMetrics: fonts[id].metrics,
fontSize: size,
fontFamily: `"${fonts[id].name}", ${fonts[id].fallback}`,
export const fontStyles: Record<StyleId, string> = {
INTER_SMALL: calcFontCss({
id: "INTER",
leading: 16.5,
size: typeScale.s,
INTER_MED: calcFontCss({
id: "INTER",
leading: 21,
size: typeScale.m,
INTER_LARGE: calcFontCss({
id: "INTER",
leading: 25,
size: typeScale.l,
INTER_XLARGE: calcFontCss({
id: "INTER",
leading: 32,
size: typeScale.xl,
export const fontFiles = Object.values(fonts).reduce(
(prev, cur) => [...prev, ...Object.values(cur.files)],

We are only using 1 font-family but this file is structured to allow more if required. It also allows for non-variable fonts. The font sizes were calculated using type-scale. Notes on why unions are used instead of enums can be found here.

The word loading

7. Load Fonts

Create a file /styles/app.css.ts. This will act as the CSS reset & where font families will be loaded. Add the following:


import { globalFontFace, globalStyle } from "@vanilla-extract/css";
import { fonts } from "./typography.css";
globalFontFace(`"${fonts.INTER.name}"`, {
fontDisplay: `optional`,
fontStyle: `normal`,
fontWeight: fonts.INTER.weights.variable,
src: `url("${fonts.INTER.files.variable}") format("${fonts.INTER.format}")`,
globalStyle("*, *::before, *::after", {
boxSizing: "border-box",
margin: 0,
padding: 0,
globalStyle("p, strong, h1, h2, h3, h4, h5, h6", {
overflowWrap: `break-word`,
fontWeight: 400,

fontDisplay: 'optional' is being used to prevent layout shifting & a flash of invisible text on page load. This also requires preloading font files. Create a file /pages/_document.tsx & add the following:


import Document, { Head, Html, Main, NextScript } from "next/document";
import React from "react";
import { fontFiles } from "../styles/typography.css";
export default class MyDocument extends Document {
static async getInitialProps(ctx: any) {
const initialProps = await Document.getInitialProps(ctx);
return { ...initialProps };
render() {
return (
<Html lang="en">
{fontFiles.map((file) => (
<link as="font" crossOrigin="anonymous" href={file} key={file} rel="preload" />
<Main />
<NextScript />

Using fontDisplay: 'optional' sets an extremely small block period & no swap period for font loading. In my experience, even on a fast connection (25 Mbps), the font won't always load in time & the page will display the fallback. If you require to always display the correct font & can accept a layout shift, fontDisplay: 'swap' might be a better setting to use. More information on font-display values can be found here.

To set the CSS reset styles in all pages within the app, import the app.css.ts into /pages/_app.ts:


import type { AppProps } from "next/app";
// highlight
import "../styles/app.css";
export default function MyApp({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />;
A baby test dummy being fired upon by machine guns while 2 people watch.

8. Test

Lets visually test the font styles to ensure everything is working correctly. Normally I would do this using Storybook but that is out of the scope of this note. Create a file /styles/test.css.ts & add the following:


import { style } from "@vanilla-extract/css";
import { fontStyles, fontWeights } from "./typography.css";
export const layout = style({
padding: `16px`,
display: `flex`,
flexDirection: `column`,
alignItems: `flex-start`,
gap: `16px`,
export const label = fontStyles.interS;
export const p = fontStyles.interM;
export const h2 = fontStyles.interL;
export const h1 = fontStyles.interXL;
export const boldText = style([
fontVariationSettings: `"wght" ${fontWeights.inter.bold}`,
color: `red`

This file creates a CSS class using each of our defined font sizes. The last class, boldText, is showing how a font style can be extended & how to use 1 of the font weights we defined earlier. To use these styles, modify the pages/index.tsx page as follows:


import type { NextPage } from "next";
import * as styles from "../styles/test.css";
const Home: NextPage = () => {
return (
<div className={styles.layout}>
<h1 className={styles.h1}>my h1</h1>
<h2 className={styles.h2}>my h2</h2>
<p className={styles.p}>my paragraph</p>
<label className={styles.label}>my label</label>
<p className={styles.boldText}>my bold text</p>
export default Home;
Placeholder text running down the screen with outlines around them.

9. Result

The end result is typography rendering at the defined size & weight with unwanted space above & below trimmed off. The outlines around the text where produced from a browser extension: Pesticide for Chrome.


I wasn't able to get a sandbox working with Next.js & vanilla-extract. As a workaround, I've created a GitHub repo.