Typography Presets in theme.json: The Complete Block Theme Guide

Typography is one of the most underestimated levers in block theme design. Get it right in theme.json and every heading, paragraph, and caption across your entire site responds consistently – no extra CSS, no per-block overrides, no surprises between screen sizes. Get it wrong and you spend hours chasing inconsistent font sizes across templates. This guide covers every typography setting available in theme.json: font family declarations with fontFace, fluid font size presets, custom font loading, and fine-grained control over line-height and letter-spacing.


Why typography belongs in theme.json (and not in CSS)

Before WordPress 6.0, theme developers controlled typography almost entirely through style.css. That worked, but it created a gap: the Site Editor had no awareness of what fonts were loaded or what sizes meant. Users could not pick a “Large” size from the block toolbar because the theme never told WordPress what “Large” actually was.

theme.json closes that gap. When you register font families and font size presets there, WordPress exposes them throughout the editor interface. Every block that has typography controls – Heading, Paragraph, Group, Cover, and dozens more – can reference your presets directly. The result is a coherent design system that editors can use without touching code.

There is also a performance angle. As part of a complete block theme setup – including the block theme development workflow and editor controls – typography in theme.json is the layer that ties the visual design to the editor’s user interface. Declaring fonts in theme.json lets WordPress manage font loading through its Font Library API (introduced in 6.5), which handles enqueueing, subsetting, and font-display swap automatically. You stop writing @font-face rules by hand and let the platform do it.

Defining typography in theme.json is not about convenience – it is about building a design system the editor can actually understand and surface to content creators.


fontFamilies and fontFace declarations

The fontFamilies array lives inside settings.typography. Each entry registers one font family and optionally declares its faces – the individual weight and style combinations that should be loaded.

Anatomy of a fontFamily entry

Each font family object requires three fields: fontFamily (the CSS value, including fallbacks), name (the human-readable label shown in the editor), and slug (the machine-readable identifier used to generate CSS custom properties). The optional fontFace array loads the actual font files.

A few things to note in this structure. The src field inside each fontFace object is an array – you can list multiple formats for broad browser support, though modern browsers all handle woff2 without fallbacks. The file:./ prefix tells WordPress the font is bundled with the theme, relative to the theme root. For variable fonts, a single face entry covers the full weight range using a range value like "100 900".

Once registered, WordPress generates a CSS custom property for each font family automatically: --wp--preset--font-family--inter for the slug inter, --wp--preset--font-family--playfair-display for playfair-display. You reference these properties in the styles section of theme.json to apply fonts to elements without writing any CSS.

Pairing fonts effectively

A typical block theme registers two to three font families: a sans-serif for body text and UI, a serif or display font for headings, and optionally a monospace font for code. Registering more than three families rarely improves the design and adds unnecessary HTTP requests if you are loading from an external source.

  • Body font: Choose a highly readable sans-serif (Inter, Source Sans, DM Sans). Set it as the base fontFamily in styles.typography.
  • Heading font: A contrasting serif or weighted sans-serif creates visual hierarchy. Apply it to styles.elements.h1 through h4.
  • Code font: A monospace font on code and pre elements improves readability for technical content. One weight is enough.

Font size presets: static and fluid

Font size presets define the named sizes that appear in the block toolbar and editor sidebar. WordPress ships with default sizes, but professional themes always replace them with a custom scale that matches the visual design.

Static font size presets

Static presets assign a fixed size to each step. They are simple, predictable, and appropriate for most situations where you control the viewport range.

Use rem values rather than px for all presets. rem respects the user’s browser font size preference – a critical accessibility requirement. Setting 1rem equals the browser default (typically 16px), so your scale is predictable without hard-coding pixel values.

The generated CSS custom properties follow the pattern --wp--preset--font-size--{slug}, so slug: "2x-large" becomes --wp--preset--font-size--2x-large. In the styles section you reference these directly: "fontSize": "var(--wp--preset--font-size--large)".

Fluid typography: font sizes that scale with the viewport

Fluid typography eliminates hard breakpoints for font sizes. Instead of jumping from 32px on mobile to 48px on desktop at a single media query, the size transitions smoothly across the full viewport range using CSS clamp(). WordPress generates the clamp formula from the min and max values you provide – you never write the clamp math yourself.

To enable fluid typography, set settings.typography.fluid to true and add a fluid object to each size preset that should scale.

When WordPress processes this, it outputs something like clamp(1.125rem, 1.125rem + ((1vw - 0.2rem) * 0.938), 1.5rem) for the large size. The exact formula uses a linear interpolation between the min and max values across the responsive range (320px to 1600px by default). You do not need to understand the math – you only need to choose sensible min and max values.

A good rule: the difference between min and max should be proportional to the size. Small body text might scale by 2–4px total. A hero heading might scale by 40–60px. Over-scaling small text creates erratic jumps; under-scaling large headings wastes the technique.

Preset Slug Min (mobile) Max (desktop) Typical use
small 0.75rem 0.875rem Captions, labels, meta
medium 0.875rem 1rem Body text, descriptions
large 1.125rem 1.5rem Lead paragraphs, H4
x-large 1.375rem 2rem H3, section subheads
2x-large 1.75rem 3rem H2, article titles
3x-large 2.25rem 4.5rem H1, hero headings

Not every size preset needs a fluid range. If your body text is 1rem and you are happy with it staying at 1rem on all screens, skip the fluid object for that preset. Setting "fluid": false on a specific preset also opts it out of fluid scaling even when global "fluid": true is set.


Custom font loading: local files vs Google Fonts

You have two options for loading custom fonts in a block theme: bundle the font files locally inside the theme, or reference them from an external source like Google Fonts. Each approach has real trade-offs in performance, privacy, and maintenance.

Loading fonts from Google Fonts via theme.json

Google Fonts URLs can be placed directly in the src array of a fontFace entry. WordPress will load them as external resources. This is straightforward to set up but comes with caveats.

The URLs in the example above point to specific woff2 files on Google’s CDN. To find the correct URL for any Google Font, visit fonts.google.com, select the weights you need, then inspect the CSS import URL – the woff2 links are embedded in the returned stylesheet.

Local fonts: the better choice for production themes

Bundling fonts locally is the preferred approach for distributed or client themes. The reasons are straightforward:

  • Privacy compliance: GDPR and other privacy regulations in some jurisdictions require disclosure of third-party data transfers. A visitor loading your page triggers a connection to Google’s servers when using Google Fonts CDN. Local fonts eliminate this entirely.
  • Performance: Removing the external DNS lookup, TLS handshake, and CDN round-trip typically saves 100–300ms on first load. On repeat visits the difference is smaller, but first-impression performance matters for SEO and user retention.
  • Reliability: Your theme fonts are not dependent on Google’s CDN uptime or any future changes to font file URLs.
  • Offline and staging environments: Local fonts work without internet access, which is important for development and client preview environments.

To use local fonts, place the woff2 files in your theme directory (a common convention is assets/fonts/{family-name}/) and reference them with the file:./ prefix. Only ship the weights you actually register – every unused font file adds to theme zip size and server disk usage.

Local fonts load faster, respect visitor privacy, and never break when a CDN changes its file structure. For production themes, there is no compelling reason to choose otherwise.

Variable fonts: one file, all the weights

Variable fonts are a single font file that encodes the full weight (and sometimes width or slant) axis. If you are building a broader design system, see how CSS custom properties in block themes let you reference these font presets throughout your template parts and patterns. Instead of loading separate files for Regular, SemiBold, and Bold, you load one file and access all weights through CSS. The file is larger than a single static weight file, but smaller than two or three static files combined.

In theme.json, declare a variable font by using a range for fontWeight: "fontWeight": "100 900". WordPress passes this through to the generated @font-face rule, which browsers interpret correctly as a variable axis range. You then reference any weight in that range from your styles.


Line-height and letter-spacing settings

WordPress exposes line-height and letter-spacing as user-controllable settings in the block editor. To make them available, you first enable them in settings.typography, then define your base values in styles.

Choosing the right line-height values

Line-height is unitless in CSS and in theme.json – it is a multiplier of the current font size. A value of 1.7 means each line of text is 1.7 times the font size tall.

  • Body text: 1.6–1.8. Longer line lengths need more vertical breathing room. For a measure of 60–75 characters, 1.7 is a reliable baseline.
  • Headings: 1.1–1.3. Large type with wide line-height looks disconnected. Tight line-height reinforces that the heading is a single unit of meaning.
  • Display / hero text: 1.0–1.15. Very large sizes (3rem+) often look best at or below 1.1.
  • Captions and metadata: 1.4–1.5. Smaller text is usually shorter in measure, needing less leading than body.

Letter-spacing: when to use it and when not to

Letter-spacing (CSS letter-spacing, also called tracking) should be applied deliberately. The most common use cases are:

  • Negative tracking on large headings: Display fonts tend to look better with slightly tighter tracking at large sizes. Values like -0.02em to -0.04em pull letterforms together without making them collide.
  • Wide tracking on uppercase labels: All-caps text at small sizes benefits from open tracking (0.08em–0.12em). This compensates for the visual density of capital letterforms set side-by-side.
  • Zero tracking on body text: Body text should almost always have 0em letter-spacing. The typeface designer has already set appropriate spacing; any addition creates unnatural gaps between letters at reading sizes.

In theme.json, letter-spacing values use the same CSS syntax as you would write in a stylesheet. Use em units rather than px so the tracking scales with the font size.

Setting "letterSpacing": true in settings.typography enables the letter-spacing control in the editor for users. If you omit this, the base letter-spacing you define in styles still applies – users just cannot override it per block.


Applying presets in the styles section

Defining presets in settings registers them. Applying them in styles is where your design actually takes shape. The styles section mirrors the block structure: global styles at the root, element styles under styles.elements, and block-specific styles under styles.blocks.

Here is a complete theme.json that ties together everything covered in this guide: font family registration with local fontFace declarations, fluid font size presets, and full typography styles applied through the element hierarchy.

Notice how the styles section references presets via CSS custom properties rather than hard-coded values. This is intentional. When a user or child theme overrides a preset value, every reference to that custom property updates automatically. Hard-coded values in styles would not inherit those overrides.

Element styles vs block styles

styles.elements targets HTML elements globally: h1, h2, h3, h4, h5, h6, a, button, caption. Any heading block that uses the default H2 level will inherit your styles.elements.h2 typography settings.

styles.blocks targets specific blocks by name, such as core/paragraph or core/post-title. Use block styles when you need typography rules that only apply to one block type and should not affect the HTML element in other contexts. For example, the post title might be an <h1> but you want it styled differently from an H1 used inside a Cover block.


Common pitfalls and how to avoid them

Typography in theme.json is well-documented but has a few sharp edges that catch most theme developers at least once.

Fluid typography silently disabled

If your fluid font sizes appear as static values in the browser, check that settings.typography.fluid is true at the root level. Individual fluid objects on each size preset are ignored unless the global flag is set. Also verify you are running WordPress 6.1 or later – fluid typography support was added in that version.

Font files not loading

The file:./ path is relative to the theme’s root directory, not to theme.json itself. If your theme root is /wp-content/themes/my-theme/ and your font is at /wp-content/themes/my-theme/assets/fonts/inter/inter-variable.woff2, the correct path in theme.json is file:./assets/fonts/inter/inter-variable.woff2.

Custom properties not generating

WordPress generates CSS custom properties for font families and sizes automatically – but only for entries that are properly structured. If --wp--preset--font-family--inter is not appearing in your page source, check that the slug field is present and contains only lowercase letters, numbers, and hyphens. Spaces and special characters in slugs are silently ignored.

Editor and front end out of sync

If fonts display correctly on the front end but not in the editor, the most common cause is a missing editor-style.css or incorrectly structured styles section. The block editor loads styles from theme.json directly, but some themes also enqueue a separate editor-style.css that can override or conflict. Check for CSS specificity conflicts between your editor stylesheet and the generated block styles.


Testing your typography system

Before shipping, run through this checklist in the Site Editor and on the front end:

  • Open Appearance > Editor > Styles and confirm all registered font families appear in the Typography panel
  • Open any Heading block and check that all font size presets appear in the toolbar
  • Resize the browser window between 320px and 1600px and watch fluid sizes scale smoothly (use browser DevTools to inspect clamp() values on the computed style)
  • Check the Network tab in DevTools: verify only the declared fontWeight variants are actually downloaded
  • Run a Lighthouse performance audit – font loading issues often appear as render-blocking resource warnings
  • Test in the editor on a post or page to confirm the editor toolbar matches front-end rendering

For fluid typography specifically, the browser’s computed styles panel is invaluable. Select any text element and inspect the font-size property – you should see the clamp() expression rather than a fixed value. Dragging the browser window will show the computed value change in real time.


Integrating with WordPress’s Font Library (WordPress 6.5+)

WordPress 6.5 introduced the Font Library – a UI in the Site Editor that lets administrators install and manage fonts without editing theme.json. Fonts installed through the Font Library are stored in the uploads directory and are registered at the site level, separate from theme fonts.

For theme developers, this means two things. First, fonts you declare in theme.json appear alongside Font Library fonts in the editor – they are not in conflict. Second, if you build a theme intended for non-technical clients, you can pre-register your font choices in theme.json while leaving room for the client to add custom fonts through the Font Library without touching any code.

The practical implication for your theme.json: keep your registered fonts focused on the design system you have built. Register only what your templates and patterns actually use. Everything else can come from the Font Library if needed.


Related guides in this series

This article is part of the Block Theme Fundamentals series. If you are working through the series, the earlier articles on fluid typography and spacing and CSS custom properties in block themes cover complementary concepts that pair directly with the typography preset system described here. The style variations guide shows how to expose alternative type pairings as one-click design choices for site editors.


Build professional block themes with full FSE control

Brndle covers block theme development end to end – from theme.json architecture to template parts, block patterns, and Global Styles. If you are building themes for clients or the directory, the guides here will help you ship faster and with more confidence.

Scroll to Top