The Difference Between add_theme_support(‘editor-styles’) and Actually Loading the Right CSS


“}

Let me write out every block with proper escaping. Done thinking; produce answer.{“title”:”The Difference Between add_theme_support(‘editor-styles’) and Actually Loading the Right CSS”,”html”:”

Every self-maintained WordPress install eventually produces the same support ticket: the post editor does not look like the published site. Serif fallback fonts in the canvas, wrong content width, links in the wrong color — while the front end renders exactly as designed. Nine times out of ten the root cause is a two-line ritual copied with one line intact. add_theme_support( 'editor-styles' ) is a declaration: it tells the block editor that your theme intends to style the editing canvas. It loads nothing. The CSS arrives through a second call — add_editor_style() — or through theme.json, which is a different mechanism entirely. This article separates those channels cleanly: what the flag actually does, what delivers bytes, how to trace which channel broke using WP-CLI and a settings-payload log, the root causes ranked by how often they occur, and a baseline setup you can verify in one sitting. If you maintain the editor for a small publishing team, this distinction is the difference between a ten-minute fix and a recurring ticket queue.

Editorial team reviewing rendered page styles around a shared office monitor
The scene of the ticket: two people, one canvas, and a Network panel with nothing in it.

The symptom: a canvas that ignores your theme

Open any draft in the block editor. The canvas — the area inside the iframe where the post body renders — falls back to the browser default serif at the browser default size. Colors and font-size presets from theme.json may still apply, which is the misleading part: the editor looks half-styled, so everyone assumes editor styles are “partly working” and starts editing CSS that was never loaded at all.

Now open your two usual instruments. Query Monitor’s Styles panel lists everything enqueued through the WP_Styles API on post.php — your editor stylesheet will not appear there, even on a healthy install. The browser Network panel shows no request for editor-style.css, also on a healthy install. Neither instrument can tell you whether editor styles loaded, because editor styles do not travel through either path. The CSS is read on the server and inlined into the canvas document; there is no enqueue to list and no HTTP request to observe.

That is the trap in one sentence: the symptom looks like a CSS bug, both instruments are silent by design, and the actual defect is one function call short of a contract. The discipline is the same one that applies when a fresh install renders “Nothing Found” instead of posts — reproduce, trace the core path, and only then change code: what to fix first when a new WordPress site says Nothing Found.

What add_theme_support( ‘editor-styles’ ) actually does: nothing, on purpose

Call it the opt-in. add_theme_support( 'editor-styles' ) writes an entry into the global $_wp_theme_features array and returns. No file is read, no path resolved, no CSS generated. You can confirm what it did with one command:

wp eval 'var_dump( current_theme_supports( "editor-styles" ) );'
bool(true)

The flag has two observable effects. First, the block editor will honor whatever the global $editor_styles array contains when the editor screen builds its settings; without the flag, that array is ignored — the registrations exist, the editor simply declines the delivery. Second, historically, it is what moved your canvas into an iframe: since WordPress 5.4, themes opting into editor styles had their post content rendered in a separate document so theme CSS could own the full cascade without fighting the editor chrome. Current versions render the canvas in an iframe regardless, but the flag remains the delivery switch.

The classic editor never needed this. TinyMCE loads add_editor_style() registrations without asking for support — which produces the other recurring confusion: a theme that styled the classic editor correctly in 2016 and “stopped” when the block editor arrived. The flag is a block-editor requirement, and the function reference says as much.

The full contract is two calls, and the ways it fails are predictable:

// functions.php
add_action( 'after_setup_theme', function () {
    add_theme_support( 'editor-styles' );              // 1. Opt in. Loads nothing.
    add_editor_style( 'assets/css/editor-style.css' ); // 2. Deliver. Registration only — still no CSS moved.
} );

Line 1 without line 2 is a promise with no shipment. Line 2 without line 1 ships to a closed dock as far as the block editor is concerned, though the classic editor will still accept it.

The three channels that actually load CSS

add_editor_style(): the file channel

add_editor_style() appends path strings to the global $editor_styles array. That is all it does at call time — it does not verify the file exists, enqueue anything, or inline anything. Delivery happens later, when the editor screen assembles its settings: core resolves each registered path against the active stylesheet’s root (the child theme’s root, if a child theme is active), reads the file server-side, and passes the raw CSS to the block editor inside the settings payload. The editor then inlines it into the canvas iframe as a <style> element.

Three consequences follow, and all of them shape how you debug:

  • No HTTP request is made for a local editor stylesheet, so the Network panel is blind to it.
  • The CSS bypasses the WP_Styles queue, so Query Monitor is blind to it.
  • A registered path that does not resolve to a real file is skipped — silently. No warning, no log line, no 404. Silence is the failure mode.

theme.json: the declarative channel

A theme.json file is the other first-class channel, and it needs neither of the two calls. Its styles and styles.blocks sections compile into CSS that core applies to the editor and the front end; WordPress 6.1 and later also accept raw CSS under styles.css. Typography and color presets arrive as CSS custom properties in both contexts. If your editor shows correct colors and preset sizes while body copy still renders in the wrong typeface, this is why: the declarative channel is working, the file channel is not. The theme.json handbook covers the full surface.

enqueue_block_editor_assets: the chrome channel

The third channel is the enqueue_block_editor_assets hook, and it does not do what people hope. Styles enqueued there load into the parent document — the editor chrome, sidebar, toolbar. The canvas is a separate iframe document and does not inherit the parent’s stylesheets. The classic miss: the team enqueues Google Fonts on that hook, the editor UI gets the font, the canvas does not, and the ticket reads “fonts broken in editor” when the font was never pointed at the canvas at all.

One adjacent flag points the opposite direction and gets conflated with this one: add_theme_support( 'wp-block-styles' ) opts the front end into core’s default block styles, which the editor already loads by default. If your blocks look styled in the editor but naked on the site, you are missing that flag, not editor styles. Opposite symptom, opposite fix — the add_theme_support reference lists everything that one function gates.

Two developers comparing a stylesheet against the rendered page on a laptop
Verification in progress: the stylesheet on one side, the canvas on the other, and neither panel volunteering information.

The trace: three checks, in order

Run these in sequence; each eliminates a class of causes.

1. Confirm the contract at the CLI.

wp eval 'var_dump( current_theme_supports( "editor-styles" ) );'
bool(true)

wp eval 'global $editor_styles; print_r( $editor_styles );'
Array
(
    [0] => assets/css/editor-style.css
)

wp eval 'var_dump( file_exists( get_stylesheet_directory() . "/assets/css/editor-style.css" ) );'
bool(true)

If the flag returns false, the registration is being ignored — start in functions.php. If the array is empty, the delivery call never ran. If file_exists returns false, you have found your silent skip and the fix is a path.

2. Log the settings payload. Drop this into a must-use plugin, load the post editor, then read debug.log:

// wp-content/mu-plugins/editor-styles-trace.php
add_filter( 'block_editor_settings_all', function ( $settings ) {
    if ( empty( $settings['styles'] ) || ! is_array( $settings['styles'] ) ) {
        error_log( '[editor-styles] settings carried no styles array.' );
        return $settings;
    }
    foreach ( $settings['styles'] as $i => $entry ) {
        $css  = isset( $entry['css'] ) ? $entry['css'] : '';
        $type = isset( $entry['__unstableType'] ) ? $entry['__unstableType'] : 'unknown';
        error_log( sprintf(
            '[editor-styles] entry %d: type=%s, %d chars, starts: %s',
            $i, $type, strlen( $css ),
            substr( preg_replace( '/\s+/', ' ', $css ), 0, 60 )
        ) );
    }
    return $settings;
} );

The filter fires for every editor instance — post, widgets, site editor — so expect multiple batches; the post editor is the one you want. On a healthy install you will see entries for theme.json-derived styles and, when the file channel works, one carrying your CSS. If the logger prints the “no styles array” line, the settings snapshot happened before your registration — cause 3 below.

3. Inspect the canvas directly. In the post editor, open DevTools, expand the iframe named editor-canvas, and read its head:

post.php (parent document)
├─ ... editor chrome: toolbar, sidebar, list view ...
└─ iframe name="editor-canvas"
   └─ #document
      ├─ <head>
      │   ├─ <style> ... core block styles, theme.json output ...
      │   └─ <style> @import url("...fonts...") body { ... } </style>
      │        └─ your editor-style.css, inlined verbatim
      └─ <body> ... post content ...

Your stylesheet shows up as an inlined style element — its contents verbatim, @import and all — not as a link to a file. If it is not there, no amount of refreshing the parent document will help; the CSS never shipped.

When you need positive proof, use a marker rule. Add this to the top of editor-style.css and load the editor:

/* Temporary: delete once the canvas turns pink. */
p { outline: 2px solid #f0f; }

Every paragraph in the canvas grows a fuchsia outline or the channel is broken. If the outline appears, any remaining mismatch is CSS specificity — not delivery. That single rule has saved me more hours than any panel in Query Monitor.

Root causes, ranked by how often they actually happen

1. The flag without the call

The most common by a wide margin. Someone read that the theme should “declare editor style support,” added the flag, and stopped reading. Symptom: total absence of editor CSS, front end unaffected. Trace: the CLI trio returns flag true, array empty. Fix: add the delivery call. Nothing else in the stack will compensate for it.

2. The right call, the wrong path

Paths in add_editor_style() resolve against the active theme’s root, not against the file doing the registering. Two usual shapes: the stylesheet lives in a subfolder but was registered as though it sat at the root ('editor-style.css' instead of 'assets/css/editor-style.css'), or the file exists only in the parent theme while a child theme is active. Trace: the file_exists check returns false. Fix: correct the path, confirm the marker rule. Remember that this failure is invisible in every panel — core skips the file without a warning.

3. Registered too late to be picked up

On the post editor screen, the settings array — including the snapshot of $editor_styles — is assembled while the page is being built, before admin_enqueue_scripts fires. A registration sitting in an admin_enqueue_scripts callback, or anywhere later in the screen’s assembly, misses the snapshot and never ships. Symptom: the CLI trio passes, the settings logger shows no entry from your file. Fix: register from functions.php, on plugins_loaded, or on after_setup_theme. Anything before screen assembly is safe.

4. Expecting style.css to mirror itself

Editor styles are a separate cascade. Nothing in core loads your front-end stylesheet into the canvas; parity is something you build. The common shape: body font, link color, and content width defined only in style.css, so the canvas receives the theme.json parts and nothing else, and the ticket reads “editor looks almost right.” Two fixes, in order of preference. Move shared tokens into theme.json, where the declarative channel handles both contexts. Or register the same file as an editor style — add_editor_style( array( 'style.css', 'assets/css/editor-style.css' ) ) — accepting that rules aimed at site wrappers that do not exist inside the canvas will simply not match. Both are defensible; the first is easier to maintain.

5. Fonts delivered to the chrome, not the canvas

Fonts enqueued on enqueue_block_editor_assets style the parent document; the canvas never sees them. Fix: deliver the font through the file channel with an @import at the very top of editor-style.css — it must be the first rule in the file or browsers ignore it — and reference the family in the body rule. The extra request inside the iframe is acceptable for an editing surface; it is not your front-end performance budget.

Developer confirming canvas styles in a browser inspector at a desk
End state: the fuchsia outline appears, the marker rule gets deleted, the ticket gets closed with a diff attached.

A baseline setup you can defend in review

The registration, with the contract stated in comments:

add_action( 'after_setup_theme', function () {
    // 1. The opt-in. Without it, the block editor ignores step 2.
    add_theme_support( 'editor-styles' );

    // 2. The delivery. Paths are relative to the active theme root,
    //    not to the file making this call.
    add_editor_style( array( 'assets/css/editor-style.css' ) );
} );

A deliberately thin stylesheet that handles what the declarative channel cannot:

/* @import must be the first rule in the file, or browsers ignore it. */
@import url("https://fonts.googleapis.com/css2?family=Source+Sans+3:wght@400;700&display=swap");

body {
    font-family: "Source Sans 3", system-ui, sans-serif;
    font-size: 1.125rem;
    line-height: 1.7;
    color: #1a1a1a;
    max-width: 720px;
    margin: 0 auto;
    padding: 0 24px;
}

a {
    color: #b3441f;
    text-decoration-thickness: 2px;
    text-underline-offset: 3px;
}

blockquote {
    border-left: 4px solid #1a1a1a;
    margin-inline-start: 0;
    padding-inline-start: 1.25rem;
}

Two cautions worth stating in review. First, relative url() references inside an inlined editor style depend on base-URL handling that has shifted between versions; if your icons vanish in the canvas but load on the front end, write the URLs out absolutely. Second, a file channel that keeps growing is a sign theme.json is being underused — colors, spacing, and preset typography belong in the declarative channel, which serves both contexts from one source of truth.

Verification, end to end

Close the loop in this order: the CLI trio returns true, a one-entry array, and true. The settings logger prints an entry with a nonzero character count for your file. The marker rule paints the canvas fuchsia. Delete the marker, reload, confirm the outline is gone. Then re-run the trio after every theme update — renamed asset folders and reshuffled parent themes are the usual regressions, and the CLI check catches both in under a minute.

FAQ

Does add_theme_support( ‘editor-styles’ ) load any CSS by itself?

No. It writes an opt-in flag into the global $_wp_theme_features array and nothing else. CSS reaches the block editor through add_editor_style() registrations — resolved against the theme root, read server-side, and inlined into the canvas iframe — or through theme.json styles, which apply without the flag. The flag’s job is to make the block editor honor the file channel; the classic editor honors it with or without the flag.

Why doesn’t Query Monitor show my editor stylesheet?

Because editor styles never pass through the WP_Styles API that Query Monitor instruments. Local editor stylesheets are read server-side, shipped inside the block editor settings payload, and inlined into the iframe canvas as style elements. There is no enqueue to list and no HTTP request to observe. To verify delivery, inspect the iframe head in DevTools or log the block_editor_settings_all filter.

Do I still need editor-style.css if my theme has theme.json?

Less than you think, but not never. theme.json covers colors, spacing, and preset typography in both the editor and the front end. Keep a thin editor stylesheet for externally hosted fonts, for parity with front-end rules that live in style.css, and for selectors theme.json cannot express. If the file keeps growing, more of it probably belongs in theme.json.

Does the classic editor need the support flag too?

No. TinyMCE loads add_editor_style() registrations without it. The flag is a block-editor requirement, which is why themes that styled the classic editor correctly needed a one-line addition once the block editor became the default editing surface.

Where this column goes next

The natural follow-up is the other side of the ledger: which parts of a growing editor-style.css belong in theme.json’s styles tree, and how to migrate them without a week of visual regressions. Same format — symptom, trace, fix, verification. If your settings log printed a variant that did not match any cause ranked above, that log line is the fastest way to narrow it down; the entry type and character count alone usually identify which channel broke. Send it along and it may open the next column.