Building Beautiful JS UIs with Inter & Outfit

Learn how to use Inter and Outfit fonts to build polished JavaScript user interfaces. Master font loading, CSS integration, and typography best practices for professional web apps.

JavaScriptbeginner
9 min read

Typography sets the tone for your entire application. The right font pairing makes your JavaScript UI look professional and polished, while poor typography makes even well-built features feel amateurish. Inter and Outfit are two of the best free fonts for web applications: Inter excels at body text and UI components, while Outfit brings personality to headings and display text. This guide shows you how to load, configure, and pair these fonts in your JavaScript projects.

Why Inter and Outfit?

Inter: The Modern UI Font

Inter was designed specifically for computer screens by Rasmus Andersson. It features tall x-height, open apertures, and carefully tuned spacing that stays readable at small sizes.

FeatureDetail
DesignerRasmus Andersson
CategorySans-serif
Weights100 (Thin) to 900 (Black)
Variable fontYes (weight and optical size axes)
Best forBody text, UI labels, buttons, forms, navigation
LicenseSIL Open Font License (free for all uses)

Outfit: The Geometric Display Font

Outfit is a geometric sans-serif that works beautifully for headings and display text. Its clean, rounded shapes pair perfectly with Inter's neutral body text.

FeatureDetail
DesignerRodrigo Fuenzalida
CategoryGeometric sans-serif
Weights100 (Thin) to 900 (Black)
Variable fontYes (weight axis)
Best forHeadings, hero text, feature titles, branding
LicenseSIL Open Font License (free for all uses)

Loading Fonts from Google Fonts

The simplest way to add Inter and Outfit is through Google Fonts. Add this to your HTML <head>:

htmlhtml
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
 
  <!-- Preconnect for faster font loading -->
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
 
  <!-- Load Inter and Outfit variable fonts -->
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Outfit:wght@400;500;600;700;800&display=swap" rel="stylesheet">
 
  <title>My App</title>
</head>
<body>
  <!-- Your content -->
</body>
</html>

Performance Optimization with Font Display

The display=swap parameter in the Google Fonts URL tells the browser to show text immediately with a fallback font, then swap to Inter/Outfit once they load. This prevents invisible text (FOIT).

csscss
/* The Google Fonts link already includes display:swap,
   but if you self-host, set it explicitly: */
@font-face {
  font-family: "Inter";
  src: url("/fonts/Inter-Variable.woff2") format("woff2");
  font-weight: 100 900;
  font-display: swap; /* Prevents invisible text during loading */
}
 
@font-face {
  font-family: "Outfit";
  src: url("/fonts/Outfit-Variable.woff2") format("woff2");
  font-weight: 100 900;
  font-display: swap;
}

Self-Hosting Fonts for Better Performance

Self-hosting eliminates the external network request to Google Fonts, improving load times and privacy:

javascriptjavascript
// Download fonts and place in your project
// public/fonts/Inter-Variable.woff2
// public/fonts/Outfit-Variable.woff2
csscss
/* fonts.css */
@font-face {
  font-family: "Inter";
  src: url("/fonts/Inter-Variable.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}
 
@font-face {
  font-family: "Outfit";
  src: url("/fonts/Outfit-Variable.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

Preloading Critical Fonts

Add preload hints so the browser starts downloading fonts immediately:

htmlhtml
<head>
  <!-- Preload the most critical font (body text) -->
  <link rel="preload" href="/fonts/Inter-Variable.woff2" as="font" type="font/woff2" crossorigin>
  <link rel="preload" href="/fonts/Outfit-Variable.woff2" as="font" type="font/woff2" crossorigin>
 
  <link rel="stylesheet" href="/css/fonts.css">
</head>

Setting Up the CSS Typography System

Create a complete typography system that pairs Inter and Outfit:

csscss
/* Base typography */
:root {
  --font-body: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  --font-heading: "Outfit", "Inter", sans-serif;
 
  /* Font sizes using a modular scale (1.25 ratio) */
  --text-xs: 0.75rem;    /* 12px */
  --text-sm: 0.875rem;   /* 14px */
  --text-base: 1rem;     /* 16px */
  --text-lg: 1.125rem;   /* 18px */
  --text-xl: 1.25rem;    /* 20px */
  --text-2xl: 1.563rem;  /* 25px */
  --text-3xl: 1.953rem;  /* 31px */
  --text-4xl: 2.441rem;  /* 39px */
  --text-5xl: 3.052rem;  /* 49px */
 
  /* Line heights */
  --leading-tight: 1.2;
  --leading-normal: 1.5;
  --leading-relaxed: 1.75;
 
  /* Font weights */
  --weight-normal: 400;
  --weight-medium: 500;
  --weight-semibold: 600;
  --weight-bold: 700;
  --weight-extrabold: 800;
}
 
/* Apply to body */
body {
  font-family: var(--font-body);
  font-size: var(--text-base);
  line-height: var(--leading-normal);
  font-weight: var(--weight-normal);
  color: #1a1a2e;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
 
/* Headings use Outfit */
h1, h2, h3, h4, h5, h6 {
  font-family: var(--font-heading);
  line-height: var(--leading-tight);
  font-weight: var(--weight-bold);
  color: #0f0f23;
}
 
h1 { font-size: var(--text-5xl); font-weight: var(--weight-extrabold); }
h2 { font-size: var(--text-4xl); }
h3 { font-size: var(--text-3xl); }
h4 { font-size: var(--text-2xl); }
h5 { font-size: var(--text-xl); }
h6 { font-size: var(--text-lg); font-weight: var(--weight-semibold); }

Building UI Components with the Font Pairing

htmlhtml
<nav class="navbar">
  <a href="/" class="navbar-brand">RuneApp</a>
  <div class="navbar-links">
    <a href="/dashboard">Dashboard</a>
    <a href="/projects">Projects</a>
    <a href="/settings">Settings</a>
  </div>
</nav>
csscss
.navbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 24px;
  height: 64px;
  background: white;
  border-bottom: 1px solid #e5e7eb;
}
 
.navbar-brand {
  font-family: var(--font-heading);
  font-size: var(--text-xl);
  font-weight: var(--weight-bold);
  color: #3b82f6;
  text-decoration: none;
}
 
.navbar-links a {
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: #6b7280;
  text-decoration: none;
  margin-left: 24px;
  transition: color 0.2s;
}
 
.navbar-links a:hover {
  color: #1f2937;
}

Card Component

csscss
.card {
  background: white;
  border-radius: 12px;
  padding: 24px;
  border: 1px solid #e5e7eb;
  transition: box-shadow 0.2s ease;
}
 
.card:hover {
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
 
.card-title {
  font-family: var(--font-heading);
  font-size: var(--text-xl);
  font-weight: var(--weight-semibold);
  color: #111827;
  margin-bottom: 8px;
}
 
.card-description {
  font-family: var(--font-body);
  font-size: var(--text-sm);
  line-height: var(--leading-relaxed);
  color: #6b7280;
}
 
.card-badge {
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
  color: #3b82f6;
  background: #eff6ff;
  padding: 2px 8px;
  border-radius: 4px;
}

Buttons with Proper Typography

csscss
.btn {
  font-family: var(--font-body);
  font-weight: var(--weight-medium);
  font-size: var(--text-sm);
  line-height: 1;
  padding: 10px 20px;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  transition: all 0.2s ease;
  letter-spacing: -0.01em;
}
 
.btn-primary {
  background: #3b82f6;
  color: white;
}
 
.btn-primary:hover {
  background: #2563eb;
  transform: translateY(-1px);
  box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
}
 
.btn-secondary {
  background: #f3f4f6;
  color: #374151;
}
 
.btn-lg {
  font-size: var(--text-base);
  padding: 14px 28px;
}

Dynamic Font Loading with JavaScript

Control font loading programmatically using the Font Loading API:

javascriptjavascript
// Check if specific fonts are loaded
document.fonts.ready.then(() => {
  console.log("All fonts loaded!");
  document.body.classList.add("fonts-loaded");
});
 
// Load specific fonts on demand
async function loadFonts() {
  const inter = new FontFace(
    "Inter",
    "url(/fonts/Inter-Variable.woff2)",
    { weight: "100 900", style: "normal" }
  );
 
  const outfit = new FontFace(
    "Outfit",
    "url(/fonts/Outfit-Variable.woff2)",
    { weight: "100 900", style: "normal" }
  );
 
  try {
    const [loadedInter, loadedOutfit] = await Promise.all([
      inter.load(),
      outfit.load()
    ]);
 
    document.fonts.add(loadedInter);
    document.fonts.add(loadedOutfit);
 
    document.body.classList.add("fonts-loaded");
    console.log("Fonts loaded successfully");
  } catch (error) {
    console.error("Font loading failed:", error);
    // Fallback fonts are already applied via font stack
  }
}
 
loadFonts();
csscss
/* Progressive enhancement: optimize display after fonts load */
body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
 
body.fonts-loaded {
  font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
 
body.fonts-loaded h1,
body.fonts-loaded h2,
body.fonts-loaded h3 {
  font-family: "Outfit", "Inter", sans-serif;
}

Using Inter and Outfit in JavaScript Frameworks

React / Next.js

javascriptjavascript
// In Next.js with next/font
import { Inter, Outfit } from "next/font/google";
 
const inter = Inter({
  subsets: ["latin"],
  variable: "--font-inter",
  display: "swap"
});
 
const outfit = Outfit({
  subsets: ["latin"],
  variable: "--font-outfit",
  display: "swap"
});
 
export default function Layout({ children }) {
  return (
    <html className={`${inter.variable} ${outfit.variable}`}>
      <body>{children}</body>
    </html>
  );
}
csscss
/* globals.css */
body {
  font-family: var(--font-inter), sans-serif;
}
 
h1, h2, h3 {
  font-family: var(--font-outfit), sans-serif;
}

Vanilla JavaScript SPA

javascriptjavascript
// app.js - Font setup for a vanilla JS app
function initializeTypography() {
  // Create and inject font styles
  const style = document.createElement("style");
  style.textContent = `
    :root {
      --font-body: "Inter", system-ui, sans-serif;
      --font-heading: "Outfit", "Inter", sans-serif;
    }
    body { font-family: var(--font-body); }
    h1, h2, h3, h4, h5, h6 { font-family: var(--font-heading); }
  `;
  document.head.appendChild(style);
 
  // Load fonts
  const link = document.createElement("link");
  link.rel = "stylesheet";
  link.href = "https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Outfit:wght@500;600;700;800&display=swap";
  document.head.appendChild(link);
}
 
document.addEventListener("DOMContentLoaded", initializeTypography);

Typography Best Practices

Optimal Font Sizes by Context

ContextFontSizeWeightLine Height
Page title (h1)Outfit2.5-3rem8001.1-1.2
Section heading (h2)Outfit1.75-2rem7001.2
Sub-heading (h3)Outfit1.25-1.5rem6001.3
Body textInter1rem (16px)4001.5-1.6
Small text / captionsInter0.875rem4001.4
ButtonsInter0.875rem5001
Navigation linksInter0.875rem5001
Code / monospaceJetBrains Mono0.875rem4001.6

Responsive Typography with Clamp

csscss
/* Fluid font sizes that scale with viewport */
h1 {
  font-family: var(--font-heading);
  font-size: clamp(2rem, 5vw, 3.5rem);
  font-weight: 800;
}
 
h2 {
  font-family: var(--font-heading);
  font-size: clamp(1.5rem, 3vw, 2.25rem);
  font-weight: 700;
}
 
body {
  font-family: var(--font-body);
  font-size: clamp(0.938rem, 1vw + 0.5rem, 1.125rem);
}

Common Mistakes to Avoid

Mistake 1: Loading Too Many Weights

htmlhtml
<!-- WRONG: Loading every weight increases page size significantly -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap" rel="stylesheet">
 
<!-- CORRECT: Load only the weights you actually use -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">

Mistake 2: Not Setting Fallback Fonts

csscss
/* WRONG: No fallbacks, text may be invisible or look wrong during loading */
body { font-family: "Inter"; }
 
/* CORRECT: System font fallbacks maintain readability */
body { font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }

Mistake 3: Using Headings Font for Body Text

csscss
/* WRONG: Outfit is geometric, harder to read in long paragraphs */
body { font-family: "Outfit", sans-serif; }
 
/* CORRECT: Inter is designed for extended reading */
body { font-family: "Inter", sans-serif; }
h1, h2, h3 { font-family: "Outfit", sans-serif; }

Real-World Example: Dashboard Layout

javascriptjavascript
function createDashboardCard(title, value, change, trend) {
  const card = document.createElement("div");
  card.className = "dashboard-card";
 
  const header = document.createElement("h3");
  header.className = "dashboard-card-title";
  header.textContent = title;
 
  const metric = document.createElement("div");
  metric.className = "dashboard-card-value";
  metric.textContent = value;
 
  const changeEl = document.createElement("span");
  changeEl.className = `dashboard-card-change ${trend}`;
  changeEl.textContent = `${trend === "up" ? "+" : ""}${change}%`;
 
  card.appendChild(header);
  card.appendChild(metric);
  card.appendChild(changeEl);
 
  return card;
}
 
// Build dashboard
const grid = document.getElementById("dashboard-grid");
const metrics = [
  { title: "Total Users", value: "12,847", change: 12.5, trend: "up" },
  { title: "Revenue", value: "$48,290", change: 8.3, trend: "up" },
  { title: "Bounce Rate", value: "32.1%", change: -4.2, trend: "down" },
  { title: "Avg. Session", value: "4m 32s", change: 2.1, trend: "up" }
];
 
metrics.forEach(m => {
  grid.appendChild(createDashboardCard(m.title, m.value, m.change, m.trend));
});
csscss
.dashboard-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 20px;
}
 
.dashboard-card {
  background: white;
  border-radius: 12px;
  padding: 24px;
  border: 1px solid #e5e7eb;
}
 
.dashboard-card-title {
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: #6b7280;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin-bottom: 8px;
}
 
.dashboard-card-value {
  font-family: var(--font-heading);
  font-size: var(--text-3xl);
  font-weight: var(--weight-bold);
  color: #111827;
  margin-bottom: 4px;
}
 
.dashboard-card-change {
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
}
 
.dashboard-card-change.up { color: #22c55e; }
.dashboard-card-change.down { color: #ef4444; }
Rune AI

Rune AI

Key Insights

  • Font roles: Use Inter for body text and UI elements; use Outfit for headings and display text
  • Performance: Self-host variable font woff2 files, preload critical fonts, and always use font-display: swap
  • CSS variables: Build a typography system with custom properties so font sizes, weights, and families stay consistent
  • System fallbacks: Always include system font fallbacks in your font stack to prevent invisible text during loading
  • Keep it minimal: Load only the font weights you actually use; four to five weights per font is sufficient for most applications
RunePowered by Rune AI

Frequently Asked Questions

Are Inter and Outfit free to use in commercial projects?

Yes. Both fonts are released under the SIL Open Font License, which allows free use in personal and commercial projects. You can self-host them, modify them, and redistribute them without any licensing fees or attribution requirements in your application UI.

How much do Inter and Outfit add to page load time?

Using the variable font versions, Inter is approximately 100KB and Outfit is approximately 50KB (woff2 compressed). With preloading and `font-display: swap`, the perceived performance impact is minimal. Loading both fonts adds roughly 150KB total, which is comparable to a single medium-sized image.

Can I mix Inter and Outfit with other fonts?

Yes, but limit your font count to two or three to keep downloads small. Inter pairs well with monospace fonts like JetBrains Mono or Fira Code for code blocks. Outfit pairs well with serif fonts like Lora or Merriweather if you want a more editorial aesthetic. The Inter + Outfit pairing is complete enough for most applications.

Should I use the variable font or individual weight files?

Use variable fonts when you need three or more weights. A single variable font file contains all weights and is usually smaller than three separate static font files. If you only need regular (400) and bold (700), static font files may be slightly smaller, but the difference is minimal.

How do I handle font flashing (FOUT/FOIT)?

Use `font-display: swap` to show text immediately in a fallback font, then swap to the custom font once loaded. Choose fallback fonts with similar metrics (x-height, width) to minimize layout shift. System UI fonts like `-apple-system` and `Segoe UI` are close matches for Inter, resulting in minimal visible change when the swap occurs.

Conclusion

The Inter and Outfit font pairing gives your JavaScript applications a professional, modern appearance with minimal effort. Inter handles body text, UI labels, and buttons with exceptional screen readability. Outfit brings visual weight and personality to headings and display text. Self-host both fonts as variable woff2 files for the best performance, set up a CSS custom properties system for consistent typography, and use the Font Loading API when you need programmatic control. The fonts are free, well-maintained, and optimized for the web.