Skip to Content
📧 Join the Teacharium waitlist to get access. 

Lesson Fonts

Authors can give a lesson two Google Fonts : a display (decorative) font for H1 and H2 headings and a body font for everything else (see Choose lesson fonts).

The self-hosted TeachariumPlayer always applies the lesson’s fonts. By default it does not load the font files, so your page is responsible for that. This lets you decide where fonts come from: Google Fonts, your own CDN, or font files bundled with your app.

SurfaceApplies the fontsLoads the font files
Teacharium editor, preview and playerâś…âś… from Google Fonts
Hosted embeddingâś…âś… from Google Fonts
LTIâś…âś… from Google Fonts
Self-hosted TeachariumPlayer✅❌ by default (opt in)
Exported ZIP✅ (when played)❌ your page loads them

If a font isn’t loaded, the browser uses the fallback in the font stack (for example system-ui, sans-serif), so the lesson still works.

Font data in the lesson

Fonts are stored on the lesson as lesson.fonts:

{ "lesson": { "id": "550e8400-e29b-41d4-a716-446655440000", "title": "Introduction to Biology", "fonts": { "display": { "family": "Lobster", "category": "display", "variants": ["400"] }, "body": { "family": "Open Sans", "category": "sans-serif", "variants": ["400", "700", "400i", "700i"] } } } }
FieldDescription
displayFont for H1 and H2. Omitted (or null) when the lesson uses the default heading font.
bodyFont for everything else. Omitted (or null) when the lesson uses the default font.
familyGoogle Fonts family name. Letters, digits and spaces only.
categorysans-serif, serif, display, handwriting or monospace. Used to pick a fallback font.
variantsThe weights the lesson uses: "400" is regular, "700" bold, and an i suffix means italic.

Lessons without custom fonts have "fonts": {} (or no fonts key, for lessons exported before fonts were available). The player treats both as “use the default font”. Invalid font entries are ignored, never fatal.

Loading the fonts

Option 1: let the player load Google Fonts

Set fontLoading: "google" and the player adds the Google Fonts stylesheet for the lesson’s fonts:

<TeachariumPlayer lesson={lessonData} widgets={widgets} config={{ fontLoading: "google" }} />

Option 2: load the fonts yourself

Keep the default fontLoading: "none" and load the fonts however you like. The only requirement is that the font-family name matches family exactly (for example "Open Sans").

The buildGoogleFontsUrl helper returns the right Google Fonts stylesheet URL for a lesson, or null when it uses the default fonts:

import { buildGoogleFontsUrl } from "@teacharium/player"; const fontsUrl = buildGoogleFontsUrl(lessonData.lesson.fonts); // "https://fonts.googleapis.com/css2?family=Lobster&family=Open+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap"

Then add it to your page, for example in a Next.js layout:

{ fontsUrl && <link rel="stylesheet" href={fontsUrl} />; }

To self-host the files instead (for privacy or offline use), declare @font-face rules using the same family names:

@font-face { font-family: "Open Sans"; font-weight: 400; font-style: normal; font-display: swap; src: url("/fonts/open-sans-400.woff2") format("woff2"); } @font-face { font-family: "Open Sans"; font-weight: 700; font-style: normal; font-display: swap; src: url("/fonts/open-sans-700.woff2") format("woff2"); } /* ...one rule per entry in `variants` ("400i" → font-style: italic) */

Exported lessons

Exported ZIPs don’t contain font files. When the lesson has custom fonts, manifest.json includes a fonts entry with each font’s details, the CSS fontFamily stack, and a ready-to-use Google Fonts URL:

{ "fonts": { "display": { "family": "Lobster", "category": "display", "variants": ["400"], "fontFamily": "\"Lobster\", system-ui, sans-serif" }, "body": { "family": "Open Sans", "category": "sans-serif", "variants": ["400", "700", "400i", "700i"], "fontFamily": "\"Open Sans\", system-ui, sans-serif" }, "googleFontsUrl": "https://fonts.googleapis.com/css2?family=Lobster&family=Open+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap" } }

How fonts are applied (CSS scoping)

Lesson fonts only affect lesson content, never the rest of your page. Here is how that works, in case you want to customize it.

Font scopes

The player marks the elements that render lesson content as font scopes: the start screen overlay (.tp-start-overlay), the step content (.tp-content) and the footer (.tp-footer-wrapper). Your headerContent and sideContent are not font scopes, so they keep your page’s font.

A font scope carries data attributes, plus the font stacks as private custom properties in its inline style:

<div class="tp-content step--welcome-0" data-tp-lesson-fonts="" data-tp-lesson-font-body="" data-tp-lesson-font-display="" style='--tp-lesson-font-body: "Open Sans", system-ui, sans-serif; --tp-lesson-font-display: "Lobster", system-ui, sans-serif' ></div>

data-tp-lesson-font-body and data-tp-lesson-font-display only appear when the lesson sets that font. Lessons with no custom fonts get no attributes at all, so your styling is untouched.

The rules

The player stylesheet (@teacharium/player/styles) maps the private properties onto public ones:

:where([data-tp-lesson-font-body]) { --tp-font-family: var(--tp-lesson-font-body); font-family: var(--tp-lesson-font-body); } :where([data-tp-lesson-font-display]) { --tp-font-display: var(--tp-lesson-font-display); } :where([data-tp-lesson-fonts]) :where(h1, h2) { font-family: var(--tp-font-display, inherit); } /* Browsers don't inherit fonts into form controls by default */ :where([data-tp-lesson-fonts]) :where(button, input, select, textarea) { font-family: inherit; }

Every selector is wrapped in :where(), so these rules have zero specificity. Two consequences:

  • Your CSS always wins. Any rule that sets font-family, --tp-font-family or --tp-font-display overrides the lesson font, even a rule aimed at the scope element itself (such as a per-step class on .tp-content) and even a bare h1 { … }.
  • Widgets keep their own fonts. A widget that sets its own font (code blocks, math, timers) is unaffected.

Customization variables

VariableSet by lesson fonts toUsed for
--tp-font-familyBody font stackPlayer and widget text (the player’s existing font variable)
--tp-font-displayDisplay font stackH1 and H2 inside a font scope

Examples:

/* Use the lesson's display font for H3 as well */ .teacharium-player :where(h3) { font-family: var(--tp-font-display, inherit); } /* Force your own brand font everywhere, ignoring lesson fonts */ .teacharium-player [data-tp-lesson-fonts] { --tp-font-family: "Brand Sans", sans-serif; --tp-font-display: "Brand Sans", sans-serif; font-family: "Brand Sans", sans-serif; }

Applying lesson fonts to your own components

If you render lesson content outside TeachariumPlayer (for example a custom step preview), use getLessonFontScopeProps to turn any element into a font scope:

import { getLessonFontScopeProps, LessonFontLoader } from "@teacharium/player"; function MyPreview({ lesson, children }) { const fonts = lesson.lesson.fonts; const scope = getLessonFontScopeProps(fonts); return ( <div {...scope} style={{ ...scope.style, padding: 16 }}> {/* Optional: load the fonts from Google Fonts */} <LessonFontLoader fonts={fonts} /> {children} </div> ); }

LessonFontLoader renders the stylesheet <link> in place rather than in <head>, so it also works inside iframes.

API summary

All exports are available from @teacharium/player (and @teacharium/player/types for the non-React helpers):

ExportDescription
PlayerConfig.fontLoading"none" (default) or "google"
LessonFonts, LessonFontTypes for lesson.fonts
LessonFontsSchemaZod schema for lesson.fonts
normalizeLessonFonts(value)Validates fonts, dropping invalid entries
buildGoogleFontsUrl(fonts)Google Fonts stylesheet URL, or null when there are no custom fonts
getFontFamilyStack(font)CSS font-family value with fallback, e.g. "Lobster", system-ui, sans-serif
getLessonFontScopeProps(fonts)Attributes and style that make an element a font scope
LessonFontLoaderReact component that loads the fonts from Google Fonts