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.
| Surface | Applies the fonts | Loads 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"]
}
}
}
}| Field | Description |
|---|---|
display | Font for H1 and H2. Omitted (or null) when the lesson uses the default heading font. |
body | Font for everything else. Omitted (or null) when the lesson uses the default font. |
family | Google Fonts family name. Letters, digits and spaces only. |
category | sans-serif, serif, display, handwriting or monospace. Used to pick a fallback font. |
variants | The 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-familyor--tp-font-displayoverrides the lesson font, even a rule aimed at the scope element itself (such as a per-step class on.tp-content) and even a bareh1 { … }. - Widgets keep their own fonts. A widget that sets its own font (code blocks, math, timers) is unaffected.
Customization variables
| Variable | Set by lesson fonts to | Used for |
|---|---|---|
--tp-font-family | Body font stack | Player and widget text (the player’s existing font variable) |
--tp-font-display | Display font stack | H1 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):
| Export | Description |
|---|---|
PlayerConfig.fontLoading | "none" (default) or "google" |
LessonFonts, LessonFont | Types for lesson.fonts |
LessonFontsSchema | Zod 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 |
LessonFontLoader | React component that loads the fonts from Google Fonts |