Why ACF block previews look broken after a WordPress update
A WordPress update moves the block editor canvas into an iframe, and theme CSS stops reaching it. Here is what actually changed, and the one bug that costs a day to find.
16 September 2026 · 8 min read · Rixio Digital
The site is fine. The front end is untouched, visitors see exactly what they saw last week. But in wp-admin the editor looks broken. The blocks have lost their styling and their layout, the fields that used to be editable on the page have collapsed into the narrow sidebar, and a few blocks show nothing at all. The people who built pages visually are now filling in form fields with no idea what the result looks like.
Nothing in the theme changed. WordPress did.
What actually changed
WordPress renders the block editor canvas inside an iframe. That started conditionally in 6.3, only on screens where every registered block type declares apiVersion 3 or higher, and became unconditional in 7.1.
An iframe is a separate document. Anything enqueued on admin_enqueue_scripts or enqueue_block_editor_assets lands in the wp-admin document outside it and never crosses the boundary. Themes that delivered editor styles that way, which was the normal pattern for years, end up with previews that get zero theme CSS.
This is not something you opt into. ACF PRO decides the block version by looking at WordPress:
$default_acf_block_version = version_compare( get_bloginfo( 'version' ), '7.1', '>=' ) ? 3 : 1;
So a site that only updates WordPress inherits V3 blocks, an iframed canvas and broken previews together, without anyone touching the theme.
Find out what you are actually running
Three answers before changing anything:
wp core version
wp plugin get advanced-custom-fields-pro --field=version
wp eval '$v=[]; foreach (acf_get_block_types() as $b) { $v[$b["acf_block_version"]."/".$b["api_version"]]=1; }
echo implode(",", array_keys($v)), " across ", count(acf_get_block_types()), " blocks\n";'
1/1is pure legacy, and the migration is straightforward.1/3is the half-migrated state that produces the worst symptoms: the theme forcesapi_version = 3but never setsacf_block_version, so the blocks run as V1 against an editor scoped for V3.3/3is migrated, and only the CSS delivery may still need work.
Do not assume the canvas is iframed. Below 7.1, a single plugin registering an apiVersion 1 or 2 block keeps the whole canvas in the wp-admin document, and a fix written for the iframe alone leaves that site with no theme CSS at all. In the browser console on an edit screen:
wp.blocks.getBlockTypes().filter(b => (b.apiVersion || 1) < 3).map(b => b.name)
The fix has to serve both cases.
The bug that costs a day
Register the blocks with acf_block_version = 3 and never set api_version by hand. ACF derives one from the other, and setting both is how sites reach that 1/3 state in the first place.
Then the previews look right on load, and go blank the instant a block is selected.
When a block is selected, ACF posts the live form state to acf/ajax/fetch-block and renders the preview from it. In that payload $block['data'] is keyed by field key, not by field name. Every $block['data']['heading'] lookup in a template returns null, and the template renders an empty shell. A temporary log line in the render callback shows it plainly: the data is field-key-keyed, $block['data']['content'] is null, and get_field('content') returns the real value.
It gets worse if nobody notices. Save from that state and the field-key shape lands in post_content, and the front end breaks the same way.
Fix it once, centrally, in the render callback. Never in the block templates:
function normalize_acf_block_data( $block ) {
if ( empty( $block['data'] ) || ! is_array( $block['data'] ) || empty( $block['id'] ) ) {
return $block;
}
if ( ! array_filter( array_keys( $block['data'] ), 'acf_is_field_key' ) ) {
return $block;
}
$fields = acf_get_block_fields( $block );
if ( empty( $fields ) ) {
return $block;
}
$block_id = acf_ensure_block_id_prefix( $block['id'] );
$values = array();
foreach ( $fields as $field ) {
$values[ $field['name'] ] = acf_get_value( $block_id, $field );
$values[ '_' . $field['name'] ] = $field['key'];
}
$block['data'] = $values;
return $block;
}
The key-shape check on the way in matters: only the editor refresh posts field-key-keyed data, and the normal post-content path must be left alone. By the time the render callback runs, acf_setup_meta() has already registered the values by name, so acf_get_value() resolves them whichever shape arrived. Rebuilding the array the way ACF builds it, with the same raw unformatted values and the same _name to field-key pairs, means templates need no changes and wpautop() is not applied twice.
Getting CSS into the canvas
enqueue_block_assets fires twice per edit screen: once for the wp-admin document, once from _wp_get_iframed_editor_assets() for the iframe. Core forces should_load_block_editor_scripts_and_styles to false around the iframe's pass, which is how you tell them apart:
$is_admin_document = apply_filters( 'should_load_block_editor_scripts_and_styles', true );
You need two builds of the same styles, and they are not interchangeable:
- the iframe gets an unscoped build, because inside the iframe there is no wp-admin to protect
- the wp-admin document gets a build scoped under
.editor-styles-wrapper .acf-block-preview
Swap them and the symptoms are obvious in hindsight. The unscoped build in the wp-admin document drops a CSS reboot onto the admin menu and wrecks its font and indentation. The scoped build inside the iframe matches nothing at all. On 7.1 and up the canvas is always iframed, so the wp-admin pass would only duplicate every sheet into the admin head for selectors that cannot match, and it should bail early.
Per-block stylesheets need the same route. ACF's own enqueue_style argument runs on enqueue_block_editor_assets and cannot reach the iframe, which produces a very specific half-working state: global typography survives, and a hero block previews at padding: 0 with a flat colour where its background image should be. Enqueue the per-block sheets in the same hook, by URL, because front-end-only handles are never registered on admin screens and a block asking for them there is a silent no-op.
The selector that silently dies
Under V1, .acf-block-body and .acf-block-preview were nested elements. Under V3 they land on the same element, so every rule written as a descendant chain across the two now matches nothing:
.acf-block-body .acf-block-preview { /* dead under V3 */ }
Specificity is the companion trap. Scoping the theme base under .editor-styles-wrapper .acf-block-preview is a three-class selector, so a later override written with a single ancestor class loses to it and appears to do nothing. Editor overrides need the full chain.
Blocks that preview as empty space
Scroll reveal animations start at opacity: 0 or a transform and wait for a class that JavaScript adds as the visitor scrolls. No theme JavaScript runs in the canvas, so that class never arrives and the whole block previews as blank space, which reads as broken rather than hidden.
Pin the end states open in an editor-only sheet:
.editor-styles-wrapper .acf-block-preview {
.fade-in-up,
.fade-in {
opacity: 1;
transform: none;
transition: none;
}
}
Inventory the project's own reveal classes rather than assuming there are two. Anything that starts at zero opacity, a transform or a clip-path and waits on JavaScript belongs in that list. A block whose fields are still empty also renders nothing at all, so give those a minimum height or an empty block is indistinguishable from a broken one.
Themes built with Vite and Tailwind
Where blocks are folders with their own block.json, the registration is declarative:
{
"apiVersion": 3,
"acf": { "blockVersion": 3, "mode": "preview" }
}
The enqueue logic is unchanged. The only difference is globbing the hashed build output in dependency order and keeping the scoped admin build out of the iframe pass.
Two things not to chase
A console warning that global-styles-css-custom-properties-inline-css was added to the iframe incorrectly is core warning about core. It appears on a stock install with any theme, and the style is cloned into the iframe regardless. Plugins produce the same line. Only act on a warning that names one of your own handles, because that one means the CSS genuinely is not arriving by the right route.
And mode: "edit" is not the cause of blank previews. ACF forces preview mode server-side under V3, so changing it in block.json does nothing at all. The cause is the field-key payload.
What to test before calling it done
- every block reports
3/3, and the wp-admin pass enqueues no canvas handles on 7.1 - click every ACF block in turn and confirm its preview stays rendered, which is the regression test for the re-key
- previews have layout and not just typography: real padding, background images, buttons where they belong
- the wp-admin menu keeps its own font and indentation
- no
apiVersiondeprecation warnings in the console - a front-end page is identical before and after, because V3 must change nothing publicly
Test on the WordPress version that forces V3, with a current ACF. That is the configuration the site is heading for anyway.
This is the kind of breakage that arrives with a routine update and has no visible cause, because nothing in the theme changed. If the editor on a site has quietly stopped showing what pages look like, that is where to look, and it is one of the things a proper maintenance arrangement is for.