Type something to search...
How to Change Fonts in a WordPress Block Theme Using theme.json?

How to Change Fonts in a WordPress Block Theme Using theme.json?

A client hands you a brand guide that specifies Source Serif 4 for headings and Inter for body text, and the site runs on a block theme like Twenty Twenty-Five. In a classic theme you would enqueue a Google Fonts stylesheet in functions.php, write some CSS, and move on. In a block theme, that approach technically works, but the fonts will not show up in the Site Editor's typography controls, editors cannot apply them to blocks, and the editor canvas will not match the front end. Block themes expect fonts to be declared in theme.json, where WordPress can load them, expose them in the UI, and generate CSS variables for them.

This article shows how to register self-hosted font files in theme.json version 3, apply them to body text, headings, and specific blocks, set up a fluid font size scale, create typography style variations, and avoid the mistakes that make fonts silently fail to load.

How theme.json Handles Typography

theme.json has two halves that matter for fonts:

  • settings.typography declares what is available: font families, font sizes, and which typography controls editors can use.
  • styles applies those presets to the site, to elements like headings and links, and to individual blocks.

When you declare a font family in settings, WordPress does three things for you. It generates @font-face rules from your fontFace definitions and prints them on the front end and in the editor. It creates a CSS custom property named --wp--preset--font-family--<slug>. And it lists the family in the font picker for every block that supports typography.

Here is the file structure this article assumes:

my-theme/
├── assets/
│   └── fonts/
│       ├── inter/
│       │   ├── Inter-Variable.woff2
│       │   └── Inter-Variable-Italic.woff2
│       └── source-serif-4/
│           └── SourceSerif4-Variable.woff2
├── styles/
├── templates/
├── style.css
└── theme.json

Download the WOFF2 files from the type foundry or Google Fonts and commit them to the theme. Self-hosting avoids a third-party request and the privacy questions discussed in whether Google Fonts are GDPR-compliant.

Registering Font Families with fontFace

Add each family to settings.typography.fontFamilies. The fontFamily value is the CSS stack, slug drives the variable name, and fontFace is an array of @font-face definitions:

{
  "$schema": "https://schemas.wp.org/trunk/theme.json",
  "version": 3,
  "settings": {
    "typography": {
      "fontFamilies": [
        {
          "name": "Inter",
          "slug": "inter",
          "fontFamily": "Inter, ui-sans-serif, system-ui, sans-serif",
          "fontFace": [
            {
              "fontFamily": "Inter",
              "fontStyle": "normal",
              "fontWeight": "100 900",
              "fontDisplay": "swap",
              "src": ["file:./assets/fonts/inter/Inter-Variable.woff2"]
            },
            {
              "fontFamily": "Inter",
              "fontStyle": "italic",
              "fontWeight": "100 900",
              "fontDisplay": "swap",
              "src": ["file:./assets/fonts/inter/Inter-Variable-Italic.woff2"]
            }
          ]
        },
        {
          "name": "Source Serif 4",
          "slug": "source-serif",
          "fontFamily": "\"Source Serif 4\", Georgia, serif",
          "fontFace": [
            {
              "fontFamily": "Source Serif 4",
              "fontStyle": "normal",
              "fontWeight": "200 900",
              "fontDisplay": "swap",
              "src": ["file:./assets/fonts/source-serif-4/SourceSerif4-Variable.woff2"]
            }
          ]
        }
      ]
    }
  }
}

Key details:

  1. file:./ paths are relative to the theme root. WordPress rewrites them to full URLs. In a child theme, the path resolves against the child theme's directory.
  2. fontWeight accepts a range like "100 900" for variable fonts. For static fonts, add one fontFace entry per weight and style, such as "400" and "700".
  3. Quote family names with spaces in the fontFamily stack, escaped as \"Source Serif 4\" in JSON.
  4. fontFamily inside fontFace must match the first name in the outer stack exactly, or the browser will never connect the face to the stack.
  5. fontDisplay defaults to fallback in WordPress if omitted. Set it explicitly; swap suits most sites.

WordPress only prints @font-face rules for fonts that are actually declared, and the browser only downloads a face when text on the page uses it, so unused weights cost nothing beyond a few bytes of CSS.

Applying Fonts to the Site, Headings, and Blocks

Declaring a family makes it available. To use it, reference the preset in styles. Theme.json supports a shorthand reference syntax, var:preset|font-family|inter, which WordPress converts into var(--wp--preset--font-family--inter):

{
  "version": 3,
  "styles": {
    "typography": {
      "fontFamily": "var:preset|font-family|inter",
      "fontSize": "var:preset|font-size|medium",
      "lineHeight": "1.65"
    },
    "elements": {
      "heading": {
        "typography": {
          "fontFamily": "var:preset|font-family|source-serif",
          "fontWeight": "600",
          "lineHeight": "1.2",
          "letterSpacing": "-0.01em"
        }
      },
      "h1": {
        "typography": {
          "fontSize": "var:preset|font-size|xx-large",
          "lineHeight": "1.1"
        }
      },
      "button": {
        "typography": {
          "fontFamily": "var:preset|font-family|inter",
          "fontWeight": "600"
        }
      }
    },
    "blocks": {
      "core/quote": {
        "typography": {
          "fontFamily": "var:preset|font-family|source-serif",
          "fontStyle": "italic"
        }
      },
      "core/code": {
        "typography": {
          "fontFamily": "ui-monospace, SFMono-Regular, Menlo, monospace",
          "fontSize": "0.9375rem"
        }
      }
    }
  }
}

The cascade works top-down: the root styles.typography sets the body defaults, elements.heading applies to every h1 through h6, the individual h1 entry overrides only the properties it lists, and blocks target a specific block wherever it appears. Editors can still override any of these per block in the editor, and those choices are stored in the block markup, not in theme.json.

You can also use the generated variables directly in style.css or block stylesheets:

.site-tagline {
  font-family: var(--wp--preset--font-family--source-serif);
  font-size: var(--wp--preset--font-size--large);
}

Defining a Fluid Font Size Scale

Font sizes live in settings.typography.fontSizes. Turn on fluid typography and WordPress generates a clamp() value for each size that scales between a minimum and maximum across the viewport range:

{
  "version": 3,
  "settings": {
    "typography": {
      "fluid": {
        "minViewportWidth": "360px",
        "maxViewportWidth": "1280px"
      },
      "defaultFontSizes": false,
      "fontSizes": [
        { "name": "Small", "slug": "small", "size": "0.875rem", "fluid": false },
        { "name": "Medium", "slug": "medium", "size": "1.0625rem", "fluid": { "min": "1rem", "max": "1.125rem" } },
        { "name": "Large", "slug": "large", "size": "1.375rem", "fluid": { "min": "1.25rem", "max": "1.5rem" } },
        { "name": "Extra Large", "slug": "x-large", "size": "2rem", "fluid": { "min": "1.625rem", "max": "2.25rem" } },
        { "name": "Huge", "slug": "xx-large", "size": "3rem", "fluid": { "min": "2.25rem", "max": "3.5rem" } }
      ]
    }
  }
}

A few points about this configuration:

  • fluid can be true or an object. true uses WordPress's default viewport range (320px to 1600px); the object form sets your own range.
  • Each size can opt out with "fluid": false, which is sensible for small sizes that should never shrink.
  • defaultFontSizes: false hides WordPress's core size presets in the editor so editors see only your scale. Set it explicitly in version 3, where core and theme size presets are handled separately and you otherwise risk two sets of sizes with similar names.
  • Sizes are in rem, so they respect the reader's browser text-size setting. That matters for WCAG 1.4.4, which requires text to scale to 200% without loss of content.

If you want the values to follow a ratio rather than hand-picked numbers, generate them with the method in how to build a modular type scale and paste the results into the min and max fields. Keep the body size at or above 1rem.

Controlling What Editors Can Change

settings.typography also controls which typography tools appear in the block sidebar. Locking down options keeps a brand consistent when many people edit the site:

{
  "version": 3,
  "settings": {
    "typography": {
      "customFontSize": false,
      "fontStyle": true,
      "fontWeight": true,
      "letterSpacing": false,
      "lineHeight": true,
      "textDecoration": false,
      "textTransform": true,
      "dropCap": false,
      "writingMode": false
    }
  }
}

With customFontSize: false, editors can only pick from your presets instead of typing arbitrary pixel sizes. You can set these per block too, under settings.blocks, for example allowing letter spacing only on core/heading.

Creating Typography Style Variations

Since WordPress 6.6, a theme can ship typography presets: JSON files in the styles/ folder that contain only typography settings and styles. Editors see them in the Site Editor under Styles → Typography and can switch font pairings with one click without changing colors.

{
  "$schema": "https://schemas.wp.org/trunk/theme.json",
  "version": 3,
  "title": "Classic Serif",
  "slug": "classic-serif",
  "settings": {
    "typography": {
      "fontFamilies": [
        {
          "name": "Literata",
          "slug": "literata",
          "fontFamily": "Literata, Georgia, serif",
          "fontFace": [
            {
              "fontFamily": "Literata",
              "fontStyle": "normal",
              "fontWeight": "200 900",
              "fontDisplay": "swap",
              "src": ["file:./assets/fonts/literata/Literata-Variable.woff2"]
            }
          ]
        }
      ]
    }
  },
  "styles": {
    "typography": {
      "fontFamily": "var:preset|font-family|literata"
    },
    "elements": {
      "heading": {
        "typography": {
          "fontFamily": "var:preset|font-family|literata"
        }
      }
    }
  }
}

Save it as styles/typography/classic-serif.json. Because the file contains only typography keys, WordPress classifies it as a typography preset rather than a full style variation. Offer two or three well-tested pairings rather than a dozen.

Using Child Themes and Fonts from the Font Library

If you are customizing a theme you did not write, do not edit its theme.json directly, because updates will overwrite your changes. Create a child theme with its own theme.json; WordPress merges it over the parent's, and your fontFamilies are combined with the parent's list. The process is covered in creating and customizing WordPress child themes.

Fonts declared in theme.json appear in the Site Editor's font management screen under the "Theme" heading, alongside any fonts installed by site administrators. That user-installed side is a separate system with its own storage, covered in how to manage fonts with the WordPress Font Library. Use theme.json for the fonts the design depends on, and leave the Font Library for site owners who want to add their own.

Troubleshooting Fonts That Do Not Load

SymptomLikely causeFix
Font appears in picker but renders as fallbackfontFace.fontFamily does not match the stack's first nameMake both names identical, including spaces and case
404 on the .woff2 requestWrong file:./ path or file not committedCheck the path relative to the theme root
Changes to theme.json do nothingSaved global styles in the database override the fileReset styles in the Site Editor or check for user overrides
Editor and front end look differentFont enqueued in PHP instead of theme.jsonMove the font to fontFace so both contexts load it
JSON ignored entirelySyntax error, such as a trailing commaValidate with the $schema in your editor

The third row catches people most often. When an editor changes typography in the Site Editor, WordPress stores those choices in a wp_global_styles post, and they take priority over theme.json. If your edits to the file seem ignored, open Styles, choose "Reset styles" from the menu, and check again.


WordPress theme.json Fonts FAQ

No. Fonts declared with fontFace in theme.json are loaded automatically on the front end and in the editor. Enqueuing them in PHP as well can cause duplicate downloads and an editor canvas that does not match the site.

The fontFace src field accepts full URLs, but WordPress expects font files rather than a Google Fonts stylesheet URL. Download the WOFF2 files and reference them with file paths in the theme, which is also better for performance and privacy.

Use version 3, introduced in WordPress 6.6. The fontFamilies and fontFace structure is the same as version 2, but version 3 changes how default font size presets are handled and is the current schema.

Register the heading font in settings.typography.fontFamilies, then set its preset reference as the fontFamily under styles.elements.heading.typography. Individual heading levels can override it under h1 through h6.

Typography choices saved in the Site Editor are stored in the database and override theme.json. Reset the global styles in the Site Editor, clear any page cache, and confirm the JSON file has no syntax errors.

Yes. Set fontWeight to a range such as 100 900 in the fontFace entry and point src at the variable WOFF2 file. Every weight in that range is then served from a single file.

Conclusion

Changing fonts in a block theme means working with theme.json rather than around it. Register each family in settings.typography.fontFamilies with fontFace entries pointing at self-hosted WOFF2 files, apply them through styles, styles.elements, and styles.blocks using preset references, and define a rem-based fluid size scale so text adapts across screen widths without ignoring user preferences.

Once that foundation is in place, lock down the typography controls you do not want editors changing, offer a couple of typography presets for flexibility, and remember that saved global styles in the database win over the file. With fonts declared this way, the editor and the front end stay in sync, and every block that supports typography can use your brand faces.

Here are some useful references for going deeper on fonts in theme.json:

  1. WordPress Developer Resources: Global Settings and Styles (theme.json) — the full reference for settings, styles, and presets.
  2. WordPress Theme Handbook: Typography — font families, font sizes, and fluid typography options.
  3. WordPress Developer Resources: Theme.json version 3 migration — what changed between schema versions.
  4. MDN Web Docs: @font-face — the CSS rule WordPress generates from your fontFace definitions.
Share :

Related Posts

Ascenders, Descenders, and Baselines: The Anatomy of a Letterform

Ascenders, Descenders, and Baselines: The Anatomy of a Letterform

You align an icon next to a button label and it looks a pixel or two too high, no matter how you adjust vertical-align. You set overflow: hidden

Continue Reading
Are Google Fonts GDPR-Compliant?

Are Google Fonts GDPR-Compliant?

In late 2022, thousands of small business owners in Germany and Austria opened letters demanding a few hundred euros in "damages" because their websi

Continue Reading
How to Audit Web Font Performance with Lighthouse?

How to Audit Web Font Performance with Lighthouse?

A client sends you a screenshot of their PageSpeed Insights report: performance score 61, LCP 3.9 seconds, and a vague list of warnings. They want to

Continue Reading