A powerful, production-ready Nuxt 3 module for managing variable fonts with automatic optimization, caching, and best practices built-in.
The module handles everything automatically: scanning packages, copying files, generating CSS, and optimizing delivery.
It's designed to be both powerful for advanced users and simple for beginners: a solution which transforms font management from a manual, error-prone process into an automated, optimized system that follows best practices by default while remaining flexible for advanced use cases.
- 🚀 Automatic Font Processing - Scans installed packages and copies only needed files
- ⚡ Smart Caching - Avoids regenerating unchanged fonts
- 🎨 Variable Font Support - Full support for all variable font axes
- 📦 Optimal Loading - Preload critical fonts, lazy load others
- đź”§ Zero Config - Works out of the box with sensible defaults
- 📊 Performance Metrics - Built-in performance tracking
- 🎯 TypeScript Support - Full type safety and IntelliSense
- 🛠️ Developer Tools - Debug panel and detailed logging
- 📱 Responsive - Adapts to connection speed and device capabilities
- ♿ Accessible - Follows best practices for font loading
modules/
fontsvar/
index.ts
types.ts
lib/
font-processor.ts
cache-manager.ts
logger.ts
fallback-metrics.ts
runtime/
composables/
useVariableFonts.ts
plugin.client.ts
server-plugin.ts
- Install the required font packages:
pnpm add @fontsource-variable/montserrat @fontsource-variable/lora
# or
npm install @fontsource-variable/montserrat @fontsource-variable/lora
-
Install the module by copying all the files to your modules/fontsvar directory
-
Add and configure the module in your
nuxt.config.ts
:
export default defineNuxtConfig({
modules: ["~/modules/fontsvar"],
variableFonts: {
fonts: [
{
package: "@fontsource-variable/montserrat",
family: "Montserrat",
variable: true,
preload: true,
weights: [400, 700],
styles: ["normal"],
subsets: ["latin"],
},
],
},
});
- Use in your components with CSS variables or the composable API
Option | Type | Default | Description |
---|---|---|---|
fonts |
FontConfig[] |
[] |
Array of font configurations |
outputDir |
string |
'public/fonts' |
Output directory for font files |
cssPath |
string |
'assets/css/variable-fonts.css' |
Generated CSS file path |
caching |
boolean |
true |
Enable caching to avoid regenerating |
cacheTTL |
number |
86400000 |
Cache time-to-live (24 hours) |
generateDocs |
boolean |
true |
Generate documentation file |
preconnect |
boolean |
true |
Add preconnect hints |
fallbacks |
boolean |
true |
Generate fallback fonts |
utilities |
boolean |
true |
Generate utility classes |
verbose |
boolean |
false |
Enable verbose logging |
Each font in the fonts
array accepts:
{
package: string // NPM package name
family: string // Font family name
variable: boolean // Is variable font
preload?: boolean // Preload for critical rendering
weights?: number[] // Weights to include
styles?: string[] // 'normal' | 'italic'
subsets?: string[] // Character subsets
axes?: string[] // Variable font axes
priority?: number // Loading priority
display?: string // font-display value
fallback?: string // Custom fallback fonts
}
<template>
<h1 class="font-montserrat">Hello World</h1>
</template>
<style>
.font-montserrat {
font-family: var(--font-montserrat);
font-weight: 700;
}
</style>
<script setup>
const fonts = useVariableFonts();
// Get all available fonts
console.log(fonts.families.value);
// Apply font dynamically
onMounted(() => {
const element = document.querySelector(".dynamic-text");
fonts.applyFont(element, {
family: "montserrat",
weight: 600,
variationSettings: { wght: 650 },
});
});
// Animate font weight
const animateHeading = async () => {
const heading = document.querySelector("h1");
await fonts.animateWeight(heading, 400, 700, 500);
};
// Preload a font
await fonts.preloadFont("lora");
</script>
<template>
<!-- Basic icon -->
<span class="material-symbols-rounded">home</span>
<!-- Filled variant -->
<span class="material-symbols-rounded filled">favorite</span>
<!-- Custom weight and size -->
<span class="material-symbols-rounded bold size-48">star</span>
<!-- Fine-tuned with CSS -->
<span
class="material-symbols-rounded"
:style="{
fontVariationSettings: `'FILL' 0.5, 'wght' 600, 'GRAD' 100, 'opsz' 40`,
}"
>
settings
</span>
</template>
// tailwind.config.js
export default {
theme: {
extend: {
fontFamily: {
sans: "var(--font-montserrat)",
serif: "var(--font-lora)",
display: "var(--font-playfair-display)",
},
},
},
};
Then use in templates:
<template>
<h1 class="font-display text-5xl font-bold">Beautiful Typography</h1>
<p class="font-serif text-lg leading-relaxed">Body text with elegant serif font</p>
</template>
const { animateAxes } = useVariableFontAnimation();
// Animate multiple axes simultaneously
await animateAxes(
element,
{
wght: { from: 400, to: 700 },
slnt: { from: 0, to: -10 },
},
500
);
<script setup>
const { useResponsiveFontSize } = useVariableFontAnimation();
// Font size scales from 16px to 24px based on viewport
const fontSize = useResponsiveFontSize(20, 16, 24, 320, 1200);
</script>
<template>
<p :style="{ fontSize }">Responsive text that scales smoothly</p>
</template>
The module automatically:
- Generates content-hashed filenames for long-term caching
- Creates fallback fonts with adjusted metrics to prevent layout shift
- Preloads critical fonts in the document head
- Uses font-display: swap for better perceived performance
- Supports subsetting via unicode-range for progressive loading
Enable verbose logging in development:
variableFonts: {
verbose: true;
}
Access the debug panel in development:
// In browser console
window.__NUXT_FONTS_DEBUG__ = true;
// Reload the page to see the debug panel
-
Limit Font Variations
- Only include weights and styles you actually use
- Use 2-3 font families maximum for most projects
-
Prioritize Critical Fonts
- Set
preload: true
for above-the-fold fonts - Use
priority
to control loading order
- Set
-
Optimize for Performance
- Enable caching in production
- Use appropriate
font-display
values - Consider connection speed with Save-Data header
-
Use Variable Fonts
- Reduce total file size
- Enable smooth animations
- Provide better design flexibility
-
Test Fallback Fonts
- Ensure fallbacks match metrics
- Test with slow network conditions
- Verify layout doesn't shift
See Configuration section above.
Main composable for font operations.
Returns:
families
: Ref<FontFamilyInfo[]> - Available font familiescurrentFont
: Ref<FontFamilyInfo | null> - Currently selected fontisLoading
: Ref - Loading stateerror
: Ref<Error | null> - Error stategetFamily(name)
: Get font family infogetVariable(family)
: Get CSS variable nameapplyFont(element, config)
: Apply font to elementpreloadFont(family)
: Preload a font familymeasureText(text, config)
: Measure text dimensionsanimateWeight(element, from, to, duration)
: Animate weightcreateFontStack(families)
: Create font stack string
Advanced animation features.
Returns:
animateAxes(element, animations, duration)
: Animate multiple axesuseResponsiveFontSize(base, min, max, minVp, maxVp)
: Responsive sizing
Access via useNuxtApp()
:
const { $fonts } = useNuxtApp();
$fonts.families; // Array of font families
$fonts.getVariable("montserrat"); // Get CSS variable
$fonts.applyFont(element, config); // Apply font
# Ensure packages are installed
npm ls @fontsource-variable/montserrat
# Check verbose logs
npm run dev -- --verbose
- Check browser DevTools Network tab for 404s
- Verify output directory matches static serving
- Check font file permissions
- Check browser support for variable fonts
- Verify axes configuration matches font capabilities
- Use fallback fonts for older browsers
# Clear cache manually
rm -rf .nuxt/variable-fonts-cache
# Or disable caching
variableFonts: {
caching: false
}
- Advanced Features:
// Color font support (COLRv1)
fonts: [{
package: '@fontsource/noto-color-emoji',
family: 'Noto Color Emoji',
colorFont: true
}]
// Automatic subsetting based on content
variableFonts: {
autoSubset: true,
scanContent: ['pages/**/*.vue', 'components/**/*.vue']
}
- Performance Monitoring:
// Built-in Core Web Vitals tracking
variableFonts: {
metrics: {
trackCLS: true,
trackFCP: true,
reportTo: '/api/metrics'
}
}
- Advanced Caching:
// Service worker integration
variableFonts: {
serviceWorker: {
strategy: 'cache-first',
maxAge: 31536000
}
}
- Font Testing Tools:
// A/B testing different font configurations
const { experimentalFont } = useVariableFonts();
experimentalFont.test("heading-font", {
control: { family: "montserrat", weight: 700 },
variant: { family: "inter", weight: 600 },
});
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch
- Submit a pull request
- Tests are encouraged
MIT License
- Built with Nuxt 3
- Fonts from Fontsource
- Inspired by @nuxt/fonts