Meta Boxes in the WordPress 7 Editor, and Safe Upgrades When Users Skip Versions

Classic meta boxes now sit in a resizable pane in the always-iframed editor. How to test them, move key fields to a sidebar panel, and migrate safely.

Three panels: meta boxes now sit in a resizable pane with no setting to turn it off, the move to a sidebar panel with registered meta, and running every upgrade step in order for users who skip versions

In the WordPress 7.1 post editor, classic meta boxes (the ones you add with add_meta_box()) still work, but they sit in their own resizable pane and the editor is always iframed, and no setting or filter we could find turns that off. Test your meta boxes in that pane today, and plan to move the fields that matter to a sidebar panel backed by registered post meta. The same release habit applies to your database: users skip versions, so your upgrade code has to cope with a jump from any old version to the current one.

In this guide

  • What changed for meta boxes in 7.1, and what the official posts do and do not say
  • A checklist for testing your plugin’s meta boxes
  • How to move a meta box to a sidebar panel, with the classic box kept for the classic editor
  • Why activation hooks are not enough for upgrades, and a migration pattern that survives skipped versions
  • How to test an upgrade from an old version, a checklist for your next release, and questions people ask

What changed for classic meta boxes in WordPress 7.1?

The post editor is now always iframed. The core team’s post Iframed Editor Changes in WordPress 7.1 says: “Starting in WordPress 7.1, the post editor is always iframed, regardless of the theme type, the block API versions of the registered blocks, or the block API versions of the blocks in the content.” The 7.1 Field Guide adds that 7.1 “completes the move to an iframed post editor, including for sites that register legacy meta boxes”, and that plugins reaching across the editor document boundary should review their JavaScript and CSS.

An iframe is a separate document inside the page. Styles and scripts in the outer admin page do not automatically apply inside it, and code that grabs the global document or window to touch the canvas is looking at the wrong document. The make post gives the fix for that: get the canvas document from an element inside it through ownerDocument and defaultView, and use useRefEffect to attach and clean up listeners. Our sister post, The Iframed Post Editor Is Unconditional in 7.1. Your Block Test Was Probably Invalid., covers that block side in detail, so we do not repeat it here.

What about the meta box area itself? The Gutenberg pull request Add ability to toggle meta box pane open and closed (merged September 21, 2025) describes the pane: a resizable, toggleable area opened from a “Meta Boxes” button, with the user’s height stored and restored, and closed by default when no preference is set. A later, still open pull request, #83930, reports that in split view a tall pane (it names ACF and Yoast) can squash the canvas, and proposes a 50 percent maximum height until the user has resized it once. Treat that as work in progress, not documented behaviour.

Is there a setting or filter to restore the old layout?

We found none. The make post and the Field Guide mention no opt-out or filter, and the Gutenberg Times Weekend Edition 371 describes the forced change as having no opt-out option. We also found no documented CSS hook for the pane. If you read otherwise somewhere, check the source before building on it. Plan for the pane as it is.

How do you test your plugin’s meta boxes in the current editor?

Run this checklist on a post, a page and every custom post type where you register a box. It takes ten minutes and catches most breakage.

  1. Open the pane. Use the “Meta Boxes” button, then drag the pane taller and shorter. Check your box is not clipped and that it scrolls.
  2. Save and reload. Change every field, click Update, reload, and confirm each value persisted. Also check a failed save path, for example a nonce that has expired.
  3. Conditional fields. Toggle any field that shows or hides others, and check it still works after you resize the pane.
  4. Scripts that assumed the old DOM. Search your JavaScript for document.querySelector, document.getElementById, window.scrollTo and getBoundingClientRect aimed at editor elements. Anything aimed at the canvas needs the ownerDocument approach above. Scripts that only touch your own box markup in the admin page need a test, not a rewrite.
  5. Style leaks. Check your admin CSS does not rely on selectors that expect canvas content and the box to share one stylesheet scope. Look at the box with a dark admin colour scheme and at 390px width too.
  6. Remembered height. Per the pull request above, the pane stores the user’s height. Test as a user who has never resized it (the case reported in #83930) as well as one who has.
  7. Console. Open the browser console with the editor loaded and fix every error your plugin raises.

To prevent regressions, keep one saved post with every field filled in and re-run this list before each release.

When should you move a meta box to a sidebar panel?

Move the fields people use on every post. A sidebar panel lives in the editor’s own interface, reads and writes through the REST API, and does not depend on the meta box pane’s size or position. Keep a classic meta box for rare, bulky or admin-only settings where a rewrite does not pay for itself. The block editor handbook page Meta Boxes in the Block Editor frames it the same way: custom meta boxes were the pre-block-editor way to extend the editor, and there are now other ways.

How do you move a meta box to a sidebar panel?

There are three parts: register the meta so the REST API exposes it, build the panel, and decide what happens to the old box.

1. Register the meta with show_in_rest and an auth_callback

The handbook says show_in_rest “ensures the data will be included in the REST API, which the block editor uses to load and save meta data.” The register_meta() reference describes auth_callback as the function called for the edit_post_meta, add_post_meta and delete_post_meta capability checks. Without your own, it falls back to is_protected_meta(), and in WordPress 7.1.3 core is_protected_meta() treats keys that start with an underscore as protected, so they are not editable over REST unless you supply a callback. Add a sanitize callback as well. This sketch follows the documented arguments and is illustrative (UNTESTED as a whole plugin):

add_action( 'init', function () {
	register_post_meta( 'post', 'acme_subtitle', array(
		'type'              => 'string',
		'single'            => true,
		'show_in_rest'      => true,
		'sanitize_callback' => 'sanitize_text_field',
		'auth_callback'     => function ( $allowed, $meta_key, $post_id ) {
			return current_user_can( 'edit_post', $post_id );
		},
	) );
} );

Two cautions from the register_post_meta() page: a custom post type needs custom-fields in its supports array for the editor to read the meta, and the REST API resolves registrations for all post types ahead of post-type-specific ones, so registering the same key two ways can make PHP and the editor disagree.

2. Add a PluginDocumentSettingPanel

The PluginDocumentSettingPanel slot lets you register UI in the Document settings sidebar, with name, title, className and icon props. The handbook’s meta example reads and writes with useEntityProp( 'postType', postType, 'meta' ). Combined, a minimal panel looks like this (illustrative, assembled from the two documented examples, UNTESTED in a browser):

import { registerPlugin } from '@wordpress/plugins';
import { PluginDocumentSettingPanel } from '@wordpress/editor';
import { useSelect } from '@wordpress/data';
import { useEntityProp } from '@wordpress/core-data';
import { TextControl } from '@wordpress/components';

const SubtitlePanel = () => {
	const postType = useSelect(
		( select ) => select( 'core/editor' ).getCurrentPostType(),
		[]
	);
	const [ meta, setMeta ] = useEntityProp( 'postType', postType, 'meta' );

	return (
		<PluginDocumentSettingPanel name="acme-subtitle" title="Subtitle">
			<TextControl
				label="Subtitle"
				value={ meta.acme_subtitle || '' }
				onChange={ ( value ) =>
					setMeta( { ...meta, acme_subtitle: value } )
				}
			/>
		</PluginDocumentSettingPanel>
	);
};

registerPlugin( 'acme-subtitle', { render: SubtitlePanel } );

Enqueue the built script on enqueue_block_editor_assets and list wp-plugins, wp-editor, wp-data, wp-core-data and wp-components as dependencies (a build with @wordpress/scripts generates that list for you). The panel saves with the post, so you no longer write nonce and save-handler code for these fields.

3. Keep the classic box for the classic editor

The add_meta_box() reference documents the signature, including a $callback_args array, but not the two compatibility flags. The handbook page above does: __back_compat_meta_box means that when the block editor is used, the box “will no longer be displayed in the meta box area, as it now only exists for backward compatibility purposes.” We confirmed that in WordPress 7.1.3 core (wp-admin/includes/template.php, read-only check in a test container): a box with this flag set is skipped when the screen is the block editor. So the flag hides the box in the block editor and keeps it for sites still on the classic editor. It is not a way to keep a box visible in the new pane.

add_meta_box(
	'acme_subtitle_box',
	'Subtitle',
	'acme_render_subtitle_box',
	'post',
	'side',
	'default',
	array( '__back_compat_meta_box' => true )
);

The sibling flag, __block_editor_compatible_meta_box set to false, does the opposite for a box you cannot yet migrate: per the handbook, WordPress will not show the box but a message saying it is not compatible with the block editor. Do not set it false on a box your users still need.

Check the result by loading a post in the block editor (the box should be gone from the pane, the panel present) and again with the classic editor (the box present, saving the same meta key). Both paths write the same acme_subtitle key, so data stays in one place.

Why are activation hooks not enough when users skip versions?

register_activation_hook() only runs when a person activates the plugin. A core post from 2010, Plugin activation hooks no longer fire for updates, explains that activation hooks no longer fire on upgrades, bulk or not, and recommends storing a version number in the database and running an upgrade procedure only when it differs from the code version. The plugin handbook’s Creating Tables with Plugins page repeats the point (“since 3.1 the activation function registered with register_activation_hook() is not called when a plugin is updated”) and shows a check on plugins_loaded that compares a stored jal_db_version option with the code’s value.

That sample runs one install function when the numbers differ. It works when you have one schema. It breaks down once you have shipped several: a user on version 1.2 who updates straight to 3.0 needs the changes from 1.3, 1.4 and so on, in order. Symptom: missing columns or options on sites that update late, usually reported as a fatal error or empty screen after an update. Check: compare the stored schema version on an affected site with your newest step. Fix and prevention: the pattern below.

What does a safe upgrade routine look like?

Use these rules:

  • Store a schema version, separate from the plugin version. Many releases change no data. Bump the schema number only when a step is added.
  • Run every step in order, from the stored version up to the newest, so any starting version reaches the current state.
  • Save the version only after each step succeeds. If step 4 fails, the stored version stays at 3 and step 4 runs again next time.
  • Make each step idempotent, meaning safe to run twice. Check whether a column exists before adding it; use dbDelta() for tables, noting the handbook’s formatting rules (each field on its own line, two spaces after PRIMARY KEY, the keyword KEY rather than INDEX).
  • Keep schema steps and data steps separate. Schema changes are fast; data rewrites can take minutes on a large site.
  • Do long data work in batches, a few hundred rows per run, with the offset stored, until the step reports it is finished.
  • Guard against two requests running the routine at once with a short-lived lock.

Here is a sketch of the runner. We ran this logic with stubbed option functions in a PHP 8.2 test container and saw the results shown below; it has not been run against a real database, and the step bodies are placeholders.

final class Acme_Upgrader {
	const OPTION = 'acme_schema_version';
	const LOCK   = 'acme_upgrade_lock';

	public static function steps(): array {
		return array(
			2 => array( __CLASS__, 'step_2_add_column' ),
			3 => array( __CLASS__, 'step_3_add_index' ),
			4 => array( __CLASS__, 'step_4_backfill' ), // batched data step
		);
	}

	public static function run(): void {
		$current = (int) get_option( self::OPTION, 1 );
		$steps   = self::steps();
		if ( $current >= max( array_keys( $steps ) ) ) {
			return;
		}
		if ( ! add_option( self::LOCK, time() ) ) {
			return; // another request is already upgrading
		}
		try {
			foreach ( $steps as $version => $step ) {
				if ( $version <= $current ) {
					continue;
				}
				if ( ! call_user_func( $step ) ) {
					break; // not finished (a batch is left); try again next run
				}
				update_option( self::OPTION, $version );
			}
		} finally {
			delete_option( self::LOCK );
		}
	}
}
add_action( 'admin_init', array( 'Acme_Upgrader', 'run' ) );

Each step returns true when done and false when more batches remain. In our stubbed run, a site starting at version 1 ran steps 2 and 3, then one batch of step 4 (version stored: 3). Two more runs finished step 4 (version stored: 4), and a further run did nothing. That is the behaviour you want.

Two caveats. A stale lock after a crash would block upgrades forever, so store a timestamp and let the routine clear a lock older than a few minutes (the sketch stores one but does not yet check it). And if you must hook on plugins_loaded as the handbook sample does, remember that is also where front-end requests run, so on large sites move the batch work to a scheduled task and keep the request-time check to one option read.

How do you test a jump from an old version to the current one?

  1. Install your oldest supported release on a throwaway site and create realistic data (a few thousand rows if you have a data step).
  2. Copy the new code over it without deactivating, as an automatic update would, so no activation hook fires.
  3. Load an admin page and watch the stored schema version climb. Confirm tables, columns and options with WP-CLI (wp option get acme_schema_version, wp db query "SHOW CREATE TABLE ...").
  4. Repeat from each earlier schema version you still support, and once more with the routine interrupted halfway (kill the request) to prove a re-run finishes cleanly.
  5. Run it twice on the finished site and check nothing changes.

The WP-CLI commands above are standard but UNTESTED for your plugin names; adapt them to your own.

What is a worked test plan for a meta box plugin on staging?

Use a staging copy, never a live site, with WordPress 7.1 and your plugin active. Follow these steps in order and write down any step that fails, with the browser, the user role and the exact field.

  1. Create a post of each type where your box appears, fill every field, and save it. This is your baseline post.
  2. Open the baseline post in the editor, click the “Meta Boxes” button, and confirm your box is present with the saved values.
  3. Edit one field of each type you ship: text, textarea, checkbox, radio, select, number, date, media picker and any repeater. Change it, click Update, reload, and confirm the new value.
  4. Open the browser console before loading the editor. Reload, edit, save, and record every error or warning that names your plugin’s files.
  5. Collapse the pane, save the post, and open the pane again. The values must still be there and still save, because a hidden box is part of real use.
  6. Drag the pane to a very small height and a very large one, then edit a field in each state. Confirm scrolling works and nothing overlaps the canvas.
  7. Repeat steps 2 to 5 as an Editor and as an Author (or whichever lower roles your users have). Check the box shows only what that role may edit, and that a save from a role without the capability is refused rather than silently stored.
  8. If your plugin reads or writes post meta in quick edit or bulk edit, change the value there, save, then open the post in the editor and confirm the editor shows the same value. Also confirm quick edit does not wipe fields it does not display.
  9. If you added a sidebar panel, confirm the panel and the classic box never both show in the block editor, and that a value saved in one shows in the other on a classic-editor site.
  10. Run the whole list once more with debugging on (WP_DEBUG and a debug log) and read the log for notices from your code.

This plan is a procedure we recommend, not a record of a test we ran against any particular plugin.

How do you tell users what changed?

If a field moved from a meta box to a sidebar panel, users who go looking for it will think it is gone. Tell them twice: in the changelog, and once inside the admin, where they will see it. Keep both short and say where the field is now.

A sample changelog line, in the action-prefix style many plugins use:

* Improve  - The Subtitle field has moved from the Meta Boxes area to the Document sidebar in the block editor. Existing values are unchanged.

A sample admin notice wording, shown once on the post edit screen after the update and dismissible:

Subtitle has moved. In the block editor it is now in the sidebar under Post, in the "Subtitle" panel. Your saved subtitles are unchanged, and the classic editor still shows the original box.

A minimal way to show it once per user is to store a dismissal flag in user meta and print the notice on admin_notices only for the post edit screens. That sketch is illustrative and UNTESTED here. Whatever you build, keep the notice to one sentence of change, one sentence of reassurance, and a dismiss control, and remove it a few releases later so it does not become permanent noise. Also mention the move in your documentation and in the support reply you keep for “where did my field go” tickets, so the answer is the same everywhere.

What should you check before the next release?

  • Every add_meta_box() screen opened in the pane, resized, saved and reloaded.
  • No script uses the global document or window for canvas elements.
  • The fields users edit most have a registered meta key, an auth_callback, a sanitize callback and a sidebar panel.
  • Migrated classic boxes carry __back_compat_meta_box and write the same key as the panel.
  • Schema version stored separately from the plugin version, with one step per change.
  • Each step idempotent, with schema and data steps split and data work batched.
  • An upgrade tested from the oldest supported version and from one in the middle, with WP_DEBUG on and no notices from your code.
  • The readme says which versions of WordPress you tested the editor on.

Questions people ask

Is there a way to turn the iframe off for a site?

We found no documented way. The make post and Field Guide describe the change as unconditional, including for legacy meta boxes. Test and adapt your code instead.

Will my classic meta boxes stop working?

The Field Guide says 7.1 completes the iframe move “including for sites that register legacy meta boxes”, not that boxes are removed. They still render in the meta box pane; test saving and any scripts.

Does __back_compat_meta_box keep my box visible?

No. It hides the box in the block editor and keeps it for the classic editor, per the handbook and the 7.1.3 core code.

Why does my registered meta not appear in the editor?

Check show_in_rest is true, the post type supports custom-fields, the key is not protected without an auth_callback, and the key is not registered a second time with different arguments.

Why did my upgrade code not run after an automatic update?

If it lives in an activation hook, it never fires on updates. Move it to a version check on a hook such as plugins_loaded or admin_init.

Should the stored version be the plugin version?

Keep a separate schema number. Plugin versions change every release; the schema changes only when your data does.

If you maintain a plugin with classic meta boxes or a database upgrade path and want a second pair of eyes before a release, get in touch through our contact page.

Varun Dubey

Written by

Varun Dubey

Varun Dubey runs Wbcom Designs, the WordPress studio he founded in India in 2009. He has spent sixteen years building on WordPress and BuddyPress, shipping client work and products such as Reign, BuddyX, Jetonomy and MediaVerse, and has been putting Claude and OpenAI workflows into production since 2023. He writes up what the studio learns along the way.

More about Varun

No comments yet