Fonts
How fonts are set up and configured in your Stera UI project.
Stera UI uses self-hosted fonts for the best performance — no external network requests, no third-party tracking, and full control over caching. When you run stera-ui init, fonts are configured automatically based on your project.
By default, Stera UI ships with Geist Sans and Geist Mono via the geist npm package.
How It Works
During stera-ui init, the CLI detects your project setup and walks you through font configuration:
- Detects existing fonts — scans for
next/fontimports,@font-facerules, and Google Fonts links - Prompts you to choose — keep your current fonts, adopt Stera UI defaults (Geist), or skip font setup entirely
- Configures fonts for your framework — Next.js projects can opt into
next/fontfor automatic optimization; other frameworks get self-hosted font files with preload guidance
Your choice is saved in components.json under the fonts field.
Font Architecture
Fonts are wired into the design system through two CSS variables in globals.css:
:root {
--font-display: Geist;
--font-body: Geist;
}All Stera UI typography utilities (st-body-md, st-heading-lg, st-display-sm, etc.) reference these variables, so changing your font is a one-line edit.
Next.js Projects
If you're using Next.js and chose Stera UI fonts during init, the CLI installs the geist package and configures next/font in your root layout:
import { GeistSans } from "geist/font/sans"
import { GeistMono } from "geist/font/mono"
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" className={`${GeistSans.variable} ${GeistMono.variable}`}>
<body>{children}</body>
</html>
);
}The CSS variables are updated to reference the next/font CSS variables:
:root {
--font-display: var(--font-geist-sans);
--font-body: var(--font-geist-sans);
}next/font handles everything else — self-hosting, preloading, @font-face generation, and size-adjust to eliminate layout shift. You do not need a fonts.css file.
Using a Different Font with Next.js
To use a different font, import localFont from next/font/local and configure it with a CSS variable:
import localFont from "next/font/local"
const inter = localFont({
src: [
{ path: "./fonts/Inter-Variable.woff2", style: "normal" },
{ path: "./fonts/Inter-Italic.woff2", style: "italic" },
],
variable: "--font-inter",
display: "swap",
})
// Apply the variable class on <html>
<html lang="en" className={inter.variable}>Then update the CSS variables:
:root {
--font-display: var(--font-inter);
--font-body: var(--font-inter);
}Vite, Remix, and Other Frameworks
For non-Next.js frameworks, stera-ui init installs the geist package, copies the variable font .woff2 files to public/fonts/, and creates a fonts.css file with @font-face declarations.
The resulting file structure:
public/
└── fonts/
├── geist/
│ ├── Geist-Variable.woff2
│ └── Geist-Italic.woff2
└── geist-mono/
├── GeistMono-Variable.woff2
└── GeistMono-Italic.woff2
Preloading
For optimal performance, add preload links to your HTML <head> so the browser starts downloading fonts before it parses CSS. The CLI prints these tags after init — only preload the regular (non-italic) variants:
<link rel="preload" href="/fonts/geist/Geist-Variable.woff2" as="font" type="font/woff2" crossorigin />
<link rel="preload" href="/fonts/geist-mono/GeistMono-Variable.woff2" as="font" type="font/woff2" crossorigin />Where you add this depends on your framework — in Vite it goes in index.html, in Remix it goes in the links export of your root route.
Using a Different Font
- Place your
.woff2files inpublic/fonts/:
public/
└── fonts/
└── inter/
├── Inter-Variable.woff2
└── Inter-Italic.woff2
- Add
@font-faceblocks instyles/fonts.css:
@font-face {
font-family: 'Inter';
src: url('/fonts/inter/Inter-Variable.woff2') format('woff2');
font-weight: 100 900;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Inter';
src: url('/fonts/inter/Inter-Italic.woff2') format('woff2');
font-weight: 100 900;
font-style: italic;
font-display: swap;
}- Update the CSS variables in
globals.css:
:root {
--font-display: Inter;
--font-body: Inter;
}Adding a Second Font
To add an additional font (for example, a monospace font for code), follow the same pattern as your primary font.
Non-Next.js — add another @font-face block in fonts.css and a new CSS variable:
@font-face {
font-family: 'JetBrains Mono';
src: url('/fonts/jetbrains-mono/JetBrainsMono-Variable.woff2') format('woff2');
font-weight: 100 900;
font-style: normal;
font-display: swap;
}Next.js — add another localFont call in your layout:
const jetbrainsMono = localFont({
src: "./fonts/JetBrainsMono-Variable.woff2",
variable: "--font-jetbrains-mono",
display: "swap",
})
<html lang="en" className={`${GeistSans.variable} ${jetbrainsMono.variable}`}>Then reference the new font wherever needed:
:root {
--font-mono: 'JetBrains Mono'; /* or var(--font-jetbrains-mono) for Next.js */
}Best Practices
- Use
.woff2format. It offers the best compression and is supported by all modern browsers. - Prefer variable fonts. A single variable font file replaces multiple static weight files, reducing total download size.
- Always set
font-display: swap. This ensures text remains visible while fonts load, preventing a flash of invisible text (FOIT). - Self-host your fonts. Avoids external network requests, eliminates third-party cookies/tracking, and gives you full control over caching headers.
- Use
next/fontin Next.js projects. It handles preloading, self-hosting, and layout shift prevention automatically.