Fixing FOUT and FOIT Problems: Complete Guide
Comprehensive guide to eliminating Flash of Unstyled Text (FOUT) and Flash of Invisible Text (FOIT). Master font-display, preloading, Font Loading API, and fallback optimization.
In Simple Terms
FOIT = invisible text (worse UX). FOUT = fallback font visible, then swap (better). Fix FOIT with font-display: swap in @font-face.Preload 1-2 critical fonts: <link rel="preload" href="font.woff2" as="font" crossorigin>. This starts download early, reducing flash duration.Minimize FOUT shift: Choose fallback fonts similar to web font (Arial for sans-serif), and use size-adjust/line-gap-override CSS to match metrics.
In this article
FOUT (Flash of Unstyled Text) and FOIT (Flash of Invisible Text) are jarring user experience issues that occur during web font loading. FOUT happens when text initially renders in a fallback system font, then suddenly switches to the web font once loaded, causing a visible style shift that disrupts reading and looks unprofessional. FOIT is even worse—text remains completely invisible for up to 3 seconds while the browser waits for fonts to download, leaving users staring at blank pages and potentially missing critical content entirely.
These problems stem from how browsers handle font loading by default. Different browsers exhibit different behaviors: Safari and older Firefox versions show invisible text (FOIT) for 3 seconds before falling back to system fonts, while Chrome and modern Firefox display fallback fonts immediately (FOUT) then swap when web fonts load. Neither behavior is ideal—FOIT creates accessibility issues and feels broken, while FOUT causes distracting layout shifts (CLS - Cumulative Layout Shift) that hurt both user experience and SEO rankings.
Modern web standards provide powerful tools to control font loading behavior and eliminate these issues. The CSS font-display property lets you specify exactly how browsers should handle the font loading period, choosing between immediate fallback display, short invisible periods, or skipping web fonts entirely if they load slowly. Font preloading with proper link rel="preload" attributes ensures critical fonts download immediately alongside HTML and CSS. The JavaScript Font Loading API provides programmatic control over font loading, allowing you to detect when fonts are ready and manage the swap timing yourself.
This comprehensive guide teaches you to completely eliminate FOUT and FOIT through strategic implementation. You'll learn how each font-display value works and when to use it, how to preload fonts correctly with crossorigin attributes and proper prioritization, how to use the Font Loading API for advanced control, how to optimize fallback fonts to minimize visual shift when swapping occurs, and how to combine these techniques into robust loading strategies that work across all browsers. By the end, you'll have mastered font loading performance and created smooth, professional typography that loads instantly without flashing or invisible text.
Understanding FOUT and FOIT
What Are FOUT and FOIT?
FOIT (Flash of Invisible Text)
- • What happens: Text is completely invisible while web font downloads
- • Duration: Up to 3 seconds in Safari, older Firefox
- • User impact: Blank page, appears broken, users may leave
- • Accessibility issue: Screen readers can't access invisible text
- • SEO impact: Poor LCP (Largest Contentful Paint) scores
- • Worst case: Text never appears if font fails to load
FOUT (Flash of Unstyled Text)
- • What happens: Text appears in fallback font, then swaps to web font
- • Visual effect: Sudden font change, layout shift, text reflow
- • User impact: Jarring visual jump, disrupts reading flow
- • CLS problem: Cumulative Layout Shift hurts Core Web Vitals
- • Brand inconsistency: Users see wrong font temporarily
- • Better than FOIT: At least content is readable immediately
Visual Comparison:
FOIT Timeline:
0s: [blank] → 0-3s: [blank] → 3s: Fallback font or web font
FOUT Timeline:
0s: Fallback font → 0.5s: SWAP to web font (visual jump)
Ideal Timeline:
0s: Fallback font → 0.1s: Smooth swap to web font (minimal shift)
Why These Problems Occur
- Network Latency: Font files take time to download (100-500ms typical, longer on slow connections)
- Browser Rendering Order: Browser parses HTML/CSS before fonts finish downloading
- Default Behavior: Browsers must decide: show nothing, or show fallback font
- File Size: Larger font files take longer to download and parse
- CDN/Server Distance: Far servers = longer download times
- No Preloading: Fonts discovered late in CSS parsing, not prioritized
Browser-Specific Behavior
| Browser | Default Behavior | Timeout Period |
|---|---|---|
| Chrome (Blink) | FOUT Shows fallback immediately, swaps when loaded | 3s block period, then swap indefinitely |
| Firefox (Gecko) | FOUT Shows fallback immediately (modern versions) | 3s block period, then swap indefinitely |
| Safari (WebKit) | FOIT Text invisible for up to 3 seconds | 3s invisible period, then shows fallback |
| Edge (Chromium) | FOUT Same as Chrome | 3s block period, then swap indefinitely |
| IE 11 | FOUT Shows fallback immediately | No timeout, swaps when loaded |
Key Takeaway
Without intervention, you'll get FOIT in Safari (invisible text) and FOUT in Chrome/Firefox (flash of different font). The font-display property gives you control over this behavior across all browsers.
Using font-display Property
font-display: swap (Recommended for Most Sites)
@font-face {
font-family: 'MyFont';
src: url('/fonts/myfont.woff2') format('woff2');
font-display: swap; /* Show fallback, swap when loaded */
}How it Works:
- • 0-100ms: Browser waits briefly for font (block period)
- • After 100ms: Shows fallback font immediately (swap period begins)
- • When loaded: Swaps to web font at any time
- • If fails: Keeps using fallback font
✓ Best for: Most websites - ensures text always visible, web font loads when ready
⚠ Caveat: May cause layout shift (FOUT) when swap occurs
font-display: optional (Best for Performance)
@font-face {
font-family: 'MyFont';
src: url('/fonts/myfont.woff2') format('woff2');
font-display: optional; /* Only use if loads fast */
}How it Works:
- • 0-100ms: Browser waits for font
- • If loads in 100ms: Uses web font
- • If takes longer: Uses fallback font for entire page
- • No swap: Font cached for next page load
✓ Best for: Performance-critical sites, progressive web apps - zero layout shift
⚠ Caveat: First-time visitors may not see web font at all
font-display: fallback (Compromise)
@font-face {
font-family: 'MyFont';
src: url('/fonts/myfont.woff2') format('woff2');
font-display: fallback; /* Short block, then swap if fast */
}How it Works:
- • 0-100ms: Text invisible (block period)
- • 100ms-3s: Shows fallback, will swap if font loads (swap period)
- • After 3s: No swap, keeps fallback even if font loads later
✓ Best for: Balance between performance and branding
⚠ Caveat: Brief invisible period, may still get FOUT
font-display: block (Avoid)
@font-face {
font-family: 'MyFont';
src: url('/fonts/myfont.woff2') format('woff2');
font-display: block; /* Wait for font - causes FOIT */
}How it Works:
- • 0-3s: Text invisible, waits for font
- • After 3s: Shows fallback if font hasn't loaded
- • When loaded: Swaps to web font at any time
✗ Avoid: Causes FOIT - text invisible for up to 3 seconds
Only use if: Brand guidelines absolutely require web font, no compromise
font-display: auto (Default - Don't Use)
Behavior: Browser decides (usually acts like "block"). Inconsistent across browsers. Always specify a value instead of relying on auto.
Decision Matrix
Use swap if: Content must be readable immediately (blogs, news, docs)
Use optional if: Performance is critical, can tolerate fallback font (apps, dashboards)
Use fallback if: You want balance, can accept brief invisible text
Avoid block unless: Brand absolutely requires web font, accessibility is not a concern
Preloading Critical Fonts
Why Preload Fonts?
By default, browsers discover fonts late in the page load process—only after downloading and parsing CSS. Preloading tells the browser to download fonts immediately, reducing the time until fonts are available by 500ms-2s.
Timeline Without Preload:
Download HTML → Parse HTML → Download CSS → Parse CSS → Discover font → Download font → Render text
Total: ~2-3 seconds
Timeline With Preload:
Download HTML + font (parallel) → Parse HTML → Download CSS → Parse CSS → Font already loaded → Render text
Total: ~1 second
Correct Preload Syntax
<!-- In HTML <head>, BEFORE any CSS -->
<link rel="preload"
href="/fonts/myfont-regular.woff2"
as="font"
type="font/woff2"
crossorigin>
<!-- Multiple fonts -->
<link rel="preload" href="/fonts/regular.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/fonts/bold.woff2" as="font" type="font/woff2" crossorigin>
<!-- CRITICAL: crossorigin is required, even for same-origin fonts! -->Common Preload Mistakes
❌ Missing crossorigin
<link rel="preload" href="/fonts/font.woff2" as="font" type="font/woff2"> <!-- Font will be downloaded TWICE - once for preload, once for @font-face -->
✓ Fix: Always add crossorigin
❌ Preloading too many fonts
<!-- Preloading 8 font files = 800KB upfront = slow page load --> <link rel="preload" href="/fonts/light.woff2" as="font" crossorigin> <link rel="preload" href="/fonts/regular.woff2" as="font" crossorigin> <link rel="preload" href="/fonts/medium.woff2" as="font" crossorigin> ...
✓ Fix: Only preload 1-2 critical fonts (usually just regular weight)
❌ Wrong MIME type
<link rel="preload" href="/fonts/font.woff2" as="font" type="font/woff"> <!-- type doesn't match actual format -->
✓ Fix: Match type to actual file format
Best Practices for Preloading
- Preload only critical font: Usually just the regular weight (400) of your primary font
- Place before CSS: Link tags should appear before stylesheet links in HTML head
- Always use crossorigin: Required for all fonts, even same-origin
- Specify correct type: font/woff2 for .woff2, font/woff for .woff
- Limit to 1-2 fonts: Preloading too many delays initial page render
- Don't preload variable fonts: Unless they're your only font (they're larger)
Font Loading API
JavaScript Control Over Font Loading
The Font Loading API gives you programmatic control over when and how fonts load, allowing you to implement custom loading strategies beyond what CSS alone can do.
Basic Font Loading
// Load a font and wait for it
document.fonts.load('400 1em MyFont').then(() => {
console.log('Font loaded!');
// Add class to trigger font usage
document.body.classList.add('fonts-loaded');
});
// Check if font is already loaded
if (document.fonts.check('400 1em MyFont')) {
console.log('Font already loaded (cached)');
}Load Multiple Fonts
// Load all fonts before showing them
Promise.all([
document.fonts.load('400 1em MyFont'),
document.fonts.load('700 1em MyFont'),
]).then(() => {
document.body.classList.add('fonts-loaded');
});
// Or use document.fonts.ready
document.fonts.ready.then(() => {
console.log('All fonts loaded');
});Practical Implementation: Two-Stage Loading
/* CSS: Hide text until font loads */
body {
font-family: Arial, sans-serif; /* Fallback */
}
.fonts-loaded body {
font-family: 'MyFont', Arial, sans-serif; /* Web font */
}
/* Optional: Add transition for smooth swap */
body {
transition: font-family 0.1s ease;
}<!-- JavaScript -->
<script>
// Load critical fonts
document.fonts.load('400 1em MyFont').then(() => {
document.body.classList.add('fonts-loaded');
});
// Set timeout fallback (in case font fails)
setTimeout(() => {
if (!document.body.classList.contains('fonts-loaded')) {
console.warn('Font timeout - using fallback');
document.body.classList.add('fonts-loaded');
}
}, 3000);
</script>Advanced: SessionStorage Caching
// Skip loading screen if fonts were loaded in this session
if (sessionStorage.getItem('fontsLoaded')) {
document.body.classList.add('fonts-loaded');
} else {
// First page load - wait for fonts
document.fonts.ready.then(() => {
sessionStorage.setItem('fontsLoaded', 'true');
document.body.classList.add('fonts-loaded');
});
}Benefit: Instant font display on subsequent pages in same session
Optimizing Fallback Fonts
Why Fallback Optimization Matters
Even with font-display: swap, there's still a visual jump when fonts swap. The larger the difference between fallback and web font metrics, the worse the layout shift (CLS). Optimizing fallbacks minimizes this shift.
Choose Similar Fallback Fonts
Match font characteristics:
- • Similar x-height: How tall lowercase letters are
- • Similar letter-spacing: Character width
- • Similar weight: Stroke thickness
Common Pairings:
• Roboto → Arial, Helvetica
• Open Sans → Arial, Helvetica
• Lato → Arial, Helvetica
• Merriweather → Georgia, Times New Roman
• Playfair Display → Georgia, Times New Roman
Using @font-face Descriptors
@font-face {
font-family: 'MyFont';
src: url('/fonts/myfont.woff2') format('woff2');
font-display: swap;
/* Adjust fallback metrics to match web font */
size-adjust: 100%; /* Scale fallback font size */
ascent-override: 90%; /* Adjust ascender height */
descent-override: 22%; /* Adjust descender depth */
line-gap-override: 0%; /* Adjust line gap */
}
/* Fallback font with adjusted metrics */
@font-face {
font-family: 'MyFont Fallback';
src: local('Arial');
size-adjust: 107%; /* Make Arial 7% larger to match */
ascent-override: 88%;
descent-override: 20%;
}Note: Browser support varies. Works in Chrome 87+, Firefox 89+
System Font Stack (Best Practice)
body {
font-family: 'MyWebFont',
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
'Helvetica Neue',
Arial,
sans-serif;
}
/* This provides:
- MyWebFont: Your web font
- -apple-system: San Francisco on macOS/iOS
- BlinkMacSystemFont: Chrome on macOS
- Segoe UI: Windows
- Roboto: Android
- Helvetica Neue: Older macOS
- Arial: Universal fallback
- sans-serif: Generic fallback
*/Advanced Loading Strategies
Strategy 1: Critical FOFT (Flash of Faux Text)
Load a subset of your font (just roman weight) immediately, then load full family later.
/* Stage 1: Load roman weight only (small file) */
@font-face {
font-family: 'MyFont';
src: url('/fonts/myfont-roman-subset.woff2') format('woff2');
font-weight: 400;
font-display: swap;
unicode-range: U+0020-007F; /* Basic Latin only */
}
/* Stage 2: Load full font family (deferred) */
@font-face {
font-family: 'MyFont';
src: url('/fonts/myfont-regular.woff2') format('woff2');
font-weight: 400;
font-display: swap;
}
@font-face {
font-family: 'MyFont';
src: url('/fonts/myfont-bold.woff2') format('woff2');
font-weight: 700;
font-display: swap;
}Strategy 2: Variable Fonts
Use a single variable font file instead of multiple weight files - reduces number of requests.
@font-face {
font-family: 'MyFont';
src: url('/fonts/myfont-variable.woff2') format('woff2-variations');
font-weight: 100 900; /* Full range in single file */
font-display: swap;
}
/* Use any weight value */
h1 { font-weight: 650; }
p { font-weight: 400; }Strategy 3: Conditional Loading
Only load web fonts on fast connections using Network Information API.
// Check connection speed
if ('connection' in navigator) {
const connection = navigator.connection;
// Only load fonts on fast connections
if (connection.effectiveType === '4g' && !connection.saveData) {
// Load fonts
document.fonts.load('400 1em MyFont').then(() => {
document.body.classList.add('fonts-loaded');
});
} else {
console.log('Slow connection - using system fonts');
}
} else {
// Fallback: load fonts normally
document.fonts.load('400 1em MyFont').then(() => {
document.body.classList.add('fonts-loaded');
});
}Best Practices
Complete Implementation Checklist
- ☐ Use font-display: swap on all @font-face declarations (prevents FOIT)
- ☐ Preload critical font only (regular weight of primary font)
- ☐ Add crossorigin to preload links (required even for same-origin)
- ☐ Optimize font files (WOFF2 format, subset to needed characters)
- ☐ Choose similar fallback fonts (minimize layout shift)
- ☐ Use system font stack as comprehensive fallback
- ☐ Self-host fonts when possible (faster than CDN for many sites)
- ☐ Limit to 2-3 font weights (each weight adds load time)
- ☐ Test on slow connections (throttle network in DevTools)
- ☐ Monitor CLS score (Cumulative Layout Shift in PageSpeed Insights)
Production-Ready Template
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<!-- Preload critical font -->
<link rel="preload"
href="/fonts/myfont-regular.woff2"
as="font"
type="font/woff2"
crossorigin>
<style>
/* Font declarations */
@font-face {
font-family: 'MyFont';
src: url('/fonts/myfont-regular.woff2') format('woff2'),
url('/fonts/myfont-regular.woff') format('woff');
font-weight: 400;
font-style: normal;
font-display: swap; /* Prevent FOIT, allow FOUT */
}
@font-face {
font-family: 'MyFont';
src: url('/fonts/myfont-bold.woff2') format('woff2'),
url('/fonts/myfont-bold.woff') format('woff');
font-weight: 700;
font-style: normal;
font-display: swap;
}
/* System font stack fallback */
body {
font-family: 'MyFont', -apple-system, BlinkMacSystemFont,
'Segoe UI', Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
</style>
</head>
<body>
<h1>Your content here</h1>
<p>Text will show immediately in fallback, swap smoothly to web font.</p>
</body>
</html>Summary: Eliminating FOUT and FOIT
FOUT and FOIT are completely preventable with modern web standards. Use font-display: swap to ensure text is always visible and swaps smoothly when fonts load. Preload your critical font (regular weight only) with proper crossorigin attribute to minimize load time. Optimize fallback fonts by choosing system fonts with similar metrics to reduce layout shift. Consider the Font Loading API for advanced control when needed.
The key is balancing performance with user experience: show text immediately in readable fallback fonts, load web fonts quickly through preloading and optimization, swap smoothly to minimize visual disruption, and always provide a timeout fallback. Test on throttled connections to ensure your strategy works for all users, not just those with fast internet.

Written by
Sarah Mitchell
Product Designer, Font Specialist

Verified by
Marcus Rodriguez
Lead Developer
