Skip to content

useFonts ​

Loads web fonts on demand. Only the faces actually chosen hit the network, and the registry ships eighteen of them.

Open in demo

Import ​

ts
import { useFonts } from '@core/fonts/useFonts'

Signature ​

ts
function useFonts(): {
  loadFont: (name: string) => Promise<void>
  unloadFont: (name: string) => void
  addCustomFont: (config: FontDefinition) => void
  setActiveFonts: (slots: { primary?: string, arabic?: string, mono?: string }) => Promise<void>
  isLoaded: (name: string) => boolean
}

Returns ​

MemberTypeDescription
setActiveFonts(slots) => Promise<void>The main API. Loads each named face and writes the matching --sa-font-{slot} variable.
loadFont(name) => Promise<void>Injects the stylesheet link and resolves once document.fonts.ready does.
unloadFont(name) => voidRemoves the link and forgets the face.
addCustomFont(config) => voidAdds a FontDefinition to the registry. Ignored if the name is already there.
isLoaded(name) => booleandocument.fonts.check('1em "Name"'). false with no document.

FontDefinition ​

KeyTypeDescription
namestringDisplay name, and the lookup key for every function here.
familystringThe full CSS font-family value, written into the variable verbatim. It has to carry a fallback.
weightsnumber[]Translated to the wght@ axis on a Google Fonts URL.
type'google' | 'local'google builds the URL from the name and weights; local requires url.
urlstringOverride for google, required for local.
script'latin' | 'arabic' | 'both'What the pickers filter on.

The three slots ​

SlotVariableDefault
primary--sa-font-primaryInter
arabic--sa-font-arabicCairo
mono--sa-font-monoJetBrains Mono

The registry ships eighteen faces: ten Latin sans, three mono, and five that cover Arabic.

Example ​

vue
<script setup lang="ts">
import { useFonts } from '@core/fonts/useFonts'

const { setActiveFonts, addCustomFont, isLoaded } = useFonts()

addCustomFont({
  name: 'Söhne',
  family: '"Söhne", system-ui, sans-serif',
  weights: [400, 600],
  type: 'local',
  url: '/fonts/soehne.css',
  script: 'latin',
})

async function apply() {
  await setActiveFonts({ primary: 'Söhne', mono: 'Fira Code' })
}
</script>

<template>
  <div class="flex items-center gap-3">
    <button type="button" @click="apply()">
      Apply fonts
    </button>
    <span v-if="isLoaded('Fira Code')">Fira Code is ready.</span>
  </div>
</template>

Notes ​

  • Deduped twice. A name already loaded returns immediately, and before injecting, the head is checked for a link with that exact href. Two racing callers cannot end up with two <link> tags.
  • loadFont awaits document.fonts.ready, so a caller that awaits it can measure text without catching the fallback face.
  • An unknown name logs a warning through console.warn and returns. It does not throw, so one bad name in a config cannot stop a boot.
  • setActiveFonts loads the named faces in parallel and resolves once all of them have. It writes each variable as soon as the face is found in the registry, so the fallback in family is what paints until the file arrives.
  • addCustomFont mutates the shared registry, so the face then appears in the font pickers too.
  • Everything that touches the DOM no-ops without a document.