--- title: Typography description: How fonts are loaded and applied across every app. type: reference related: - /en/docs/packages/design-system/colors --- # Typography `packages/design-system/lib/fonts.ts` exports a single className string that every app puts on its `` element: ```ts title="packages/design-system/lib/fonts.ts" import { cn } from '@kreogen/design-system/lib/utils'; import { GeistMono } from 'geist/font/mono'; import { GeistSans } from 'geist/font/sans'; export const fonts = cn( GeistSans.variable, GeistMono.variable, 'touch-manipulation font-sans antialiased' ); ``` ```tsx title="apps/app/app/layout.tsx" ``` Fonts come from the [`geist`](https://www.npmjs.com/package/geist) npm package, not from `next/font/google`. The files are installed as a dependency and served from your own origin, so there is no request to Google's CDN on first paint, nothing to configure in a Content Security Policy, and no third-party availability in the critical path. ## How it reaches Tailwind `GeistSans.variable` and `GeistMono.variable` are classNames that define `--font-geist-sans` and `--font-geist-mono`. `globals.css` maps those onto Tailwind's font tokens: ```css title="packages/design-system/styles/globals.css" @theme inline { --font-sans: var(--font-geist-sans); --font-mono: var(--font-geist-mono); } ``` Which is what makes `font-sans` and `font-mono` resolve, and why `fonts` carries `font-sans` itself — the variables are declared by the class, so the element that declares them is the natural place to apply the default. `antialiased` and `touch-manipulation` ride along: the first because these are variable fonts rendered light-on-dark as often as the reverse, the second because it removes the 300ms tap delay on touch devices. ## Changing the font Install it, then swap the imports. A variable font from [`fontsource`](https://fontsource.org) works the same way, as does a local file through `next/font/local`: ```ts title="packages/design-system/lib/fonts.ts" import localFont from 'next/font/local'; const sans = localFont({ src: './fonts/YourFont.woff2', variable: '--font-geist-sans', display: 'swap', }); export const fonts = cn(sans.variable, 'touch-manipulation font-sans antialiased'); ``` Keep the CSS variable names, or update the `@theme inline` mapping to match. Change one without the other and `font-sans` falls back to the system stack — which looks close enough to be missed in review and wrong on every screen. `next/font` requires the loader to be called at module scope with literal arguments. Building the config object dynamically fails at build time with a message about the call site rather than about the value. ## Adding a third family `fonts` is a plain string, so a heading or display face is one more variable: ```ts export const fonts = cn( GeistSans.variable, GeistMono.variable, display.variable, 'touch-manipulation font-sans antialiased' ); ``` Then add `--font-display: var(--font-display)` inside `@theme inline` to get a `font-display` utility. Every family costs a font file on first paint, so add them because a design calls for them, not in case. ## Prose `@tailwindcss/typography` is loaded as a plugin, and its `--tw-prose-*` variables are pointed at the design system's own tokens at the bottom of `globals.css`. So `prose` blocks — long-form marketing copy, CMS content, legal pages — follow the theme and dark mode rather than carrying their own greys. --- For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md) For an index of all available documentation, see [/llms.txt](/llms.txt)