Real-Time Collaboration in the Block Editor

Real-time collaboration (RTC) in the blockBlock Block is the abstract term used to describe units of markup that, composed together, form the content or layout of a webpage using the WordPress editor. The idea combines concepts of what in the past may have achieved with shortcodes, custom HTML, and embed discovery into a single consistent API and user experience. editor allows multiple users to edit content simultaneously by utilizing Yjs.

This dev notedev note Each important change in WordPress Core is documented in a developers note, (usually called dev note). Good dev notes generally include a description of the change, the decision that led to this change, and a description of how developers are supposed to work with that change. Dev notes are published on Make/Core blog during the beta phase of WordPress release cycle. Publishing dev notes is particularly important when plugin/theme authors and WordPress developers need to be aware of those changes.In general, all dev notes are compiled into a Field Guide at the beginning of the release candidate phase. covers three important aspects of the collaboration system that pluginPlugin A plugin is a piece of software containing a group of functions that can be added to a WordPress website. They can extend functionality or add new features to your WordPress websites. WordPress plugins are written in the PHP programming language and integrate seamlessly with WordPress. These can be free in the WordPress.org Plugin Directory https://wordpress.org/plugins/ or can be cost-based plugin from a third-party. and theme developers should be aware of:

  • How metaMeta Meta is a term that refers to the inside workings of a group. For us, this is the team that works on internal WordPress sites like WordCamp Central and Make WordPress. boxes affect collaboration mode
  • The sync.providers filterFilter Filters are one of the two types of Hooks https://codex.wordpress.org/Plugin_API/Hooks. They provide a way for functions to modify data of other functions. They are the counterpart to Actions. Unlike Actions, filters are meant to work in an isolated manner, and should never have side effects such as affecting global variables and output. for customized sync transport
  • Common issues when building plugins that can run in a collaborative environment

Collaboration is disabled when meta boxes are present

The Problem

Classic WordPress meta boxes are not synced by the real-time collaboration system. To avoid data loss, collaboration is disabled when meta boxes are detected on a post.

Locked post modal when someone takes over a post
Locked post modal when trying to take over a post

What developers need to know

To allow collaboration, consider migrating meta box functionality to registered post meta with show_in_rest set to true, and use sidebarSidebar A sidebar in WordPress is referred to a widget-ready area used by WordPress themes to display information that is not a part of the main content. It is not always a vertical column on the side. It can be a horizontal rectangle below or above the content area, footer, header, or any where in the theme. plugins or block-based alternatives that read from WordPress data stores.

For example:

register_post_meta( 'post', 'example_subtitle', [
'show_in_rest' => true, // Required for syncing.
'single' => true,
'type' => 'string',
'revisions_enabled' => true, // Recommended to track via revision history.
] );

For more details on migrating from meta boxes, see the Meta Boxes guide in the Block Editor Handbook.


The sync.providers filter: Customizing the sync transport layer

Overview

The @wordpress/sync package uses a provider-based architecture for syncing collaborative editing data. By default, WordPress ships with an HTTPHTTP HTTP is an acronym for Hyper Text Transfer Protocol. HTTP is the underlying protocol used by the World Wide Web and this protocol defines how messages are formatted and transmitted, and what actions Web servers and browsers should take in response to various commands. polling provider. The sync.providers filter allows plugins to replace or extend the transport layer. For example, a plugin could switch from HTTP polling to WebSockets for lower-latency collaboration.


How it works

The filter is applied during provider initialization:

const filteredProviderCreators = applyFilters(
'sync.providers',
getDefaultProviderCreators() // array of provider creators
);

A provider creator is a function that accepts a ProviderCreatorOptions object (containing the Yjs ydoc, awareness, objectType, and objectId) and returns a ProviderCreatorResult with destroy and on methods. The destroy method is called when the provider is no longer needed, and the on method allows the editor to listen for connection status events (connecting, connected, disconnected).


Example: WebSocket provider

The following example replaces the default HTTP polling provider with a WebSocket-based transport using the y-websocket library:

import { addFilter } from '@wordpress/hooks';
import { WebsocketProvider } from 'y-websocket';

/**
* Create a WebSocket provider that connects a Yjs document
* to a WebSocket server for real-time syncing.
*/
function createWebSocketProvider( { awareness, objectType, objectId, ydoc } ) {
const roomName = `${ objectType }-${ objectId ?? 'collection' }`;
const serverUrl = 'wss://example.com/';

const provider = new WebsocketProvider(
serverUrl,
roomName,
ydoc,
{ awareness }
);

return {
destroy: () => {
provider.destroy();
},
on: ( eventName, callback ) => {
provider.on( eventName, callback );
},
};
}

addFilter( 'sync.providers', 'my-plugin/websocket-provider', () => {
return [ createWebSocketProvider ];
} );


What developers need to know

  • The sync.providers filter is only applied when real-time collaboration is enabled.
  • Return an empty array to disable collaboration entirely.
  • Return a custom array to replace the default HTTP polling provider with your own transport (e.g., WebSockets, WebRTC).

Common issues when building plugins compatible with real-time collaboration

When real-time collaboration is active, all connected editors share the same underlying data state via Yjs. Plugins that interact with post data, especially custom post meta, need to follow certain patterns to avoid sync issues

Syncing custom post meta values

In addition to being registered, custom meta field UIUI User interface must be consumed from the WordPress data store and passed to controlled input components.
Always derive the input value directly from the WordPress data store via useSelect. In addition, use value instead of defaultValue on input components so the input always reflects the current data store state.

const metaValue = useSelect(
select => select( 'core/editor' ).getEditedPostAttribute( 'meta' )?.example_subtitle,
[]
);

<input
value={ metaValue || '' }
onChange={ event => {
editPost( { meta: { example_subtitle: event.target.value } } );
} }
/>

Avoiding local component state for shared data

When building a plugin UI that reads from the WordPress data store, avoid copying that data into local ReactReact React is a JavaScript library that makes it easy to reason about, construct, and maintain stateless and stateful user interfaces. https://reactjs.org/. state with useState. This applies to any shared data, such as post meta or block attributes. Doing so disconnects your component from the shared collaborative state: updates from other clients will update the store, but your component wonโ€™t reflect them after the initial render, leading to stale or conflicting data.

Blocks with side effects on insertion

Custom blocks that trigger side effects on insertion will trigger that side effect for all connected collaborators, since block content syncs immediately upon insertion.

For example, instead of auto-opening a modal when a block is inserted, show a placeholder with a button that opens the modal on click. This ensures side effects are intentional and local to the user taking the action.


Credits

Props @czarate, @alecgeatches, @maxschmeling, @paulkevan, and @shekharwagh for building real-time collaboration in the block editor alongside @ingeniumed, and for technical review and proofreading of this dev note.

Parts of this work are derived from contributions made by @dmonad inย this PR, and utilizes his Yjs library.

Props toย @wildworks and @tyxlaย for proofreading this dev note.

#dev-notes, #dev-notes-7-0, #7-0

Pseudo-element support for blocks and their variations in theme.json

WordPress 7.0 adds support for pseudo-class selectors (:hover, :focus, :focus-visible, and :active) directly on blocks and their style variations in theme.json. Previously, this was only possible for HTMLHTML HyperText Markup Language. The semantic scripting language primarily used for outputting content in web browsers. elements like button and link under the styles.elements key. BlockBlock Block is the abstract term used to describe units of markup that, composed together, form the content or layout of a webpage using the WordPress editor. The idea combines concepts of what in the past may have achieved with shortcodes, custom HTML, and embed discovery into a single consistent API and user experience.-level interactive states could only be achieved through custom CSSCSS Cascading Style Sheets..

Variation-level pseudo-selectors

Block style variations can also define interactive states. This is particularly useful for variations like โ€œOutlineโ€ that have distinct visual styles requiring different hover behaviors:

{
    "styles": {
        "blocks": {
            "core/button": {
                "variations": {
                    "outline": {
                        "color": {
                            "background": "transparent",
                            "text": "currentColor"
                        },
                        ":hover": {
                            "color": {
                                "background": "currentColor",
                                "text": "white"
                            }
                        }
                    }
                }
            }
        }
    }
}

  • This is a theme.json-only APIAPI An API or Application Programming Interface is a software intermediary that allows programs to interact with each other and share data in limited, clearly defined ways.. There is no new UIUI User interface in Global Styles for these states in 7.0. Work on this is happening at #38277 and will be added in a future release.
  • The supported pseudo-selectors for core/button are: :hover, :focus, :focus-visible, and :active. Any others will be ignored.
  • Pseudo-selectors defined at the block level and at the variation level are independent โ€” you can define both without conflictconflict A conflict occurs when a patch changes code that was modified after the patch was created. These patches are considered stale, and will require a refresh of the changes before it can be applied, or the conflicts will need to be resolved..

See #64263 for more details

Props to @scruffian, @onemaggie for the implementation

Props to @mikachan, @scruffian for technical review and proofreading.

#7-0, #dev-notes, #dev-notes-7-0

Changes to the Interactivity API in WordPress 7.0

New watch() function

WordPress 7.0 introduces a watch() function in the @wordpress/interactivity package. It subscribes to changes in any reactive value accessed inside a callback, re-running the callback whenever those values change.

Currently, the Interactivity APIAPI An API or Application Programming Interface is a software intermediary that allows programs to interact with each other and share data in limited, clearly defined ways. provides data-wp-watch as a directive tied to a DOM elementโ€™s lifecycle for reacting to state changes. However, there is no programmatic API to observe those changes independently of the DOM, for example, to run side effects at the store level, set up logging, or synchronize state between stores. The watch() function fills this gap.

import { store, watch } from '@wordpress/interactivity';

const { state } = store( 'myPlugin', {
    state: {
        counter: 0,
    },
} );

// Runs immediately and re-runs whenever `state.counter` changes.
watch( () => {
    console.log( 'Counter is ' + state.counter );
} );

The function returns an unwatch callback that stops the watcher:

const unwatch = watch( () => {
    console.log( 'Counter is ' + state.counter );
} );

// Later, to stop watching:
unwatch();

The callback can also return a cleanup function. This cleanup runs before each re-execution and when the watcher is disposed of via unwatch():

const unwatch = watch( () => {
    const handler = () => { /* ... */ };
    document.addEventListener( 'click', handler );

    return () => {
        document.removeEventListener( 'click', handler );
    };
} );

See #75563 for more details.

Props to @luisherranz for the implementation.

Deprecated state.navigation properties in core/router

The state.navigation.hasStarted and state.navigation.hasFinished properties in the core/router store were internal implementation details used for the loading bar animation. These were never intended to be part of the public API.

Starting in WordPress 7.0, accessing state.navigation from the core/router store is deprecated and will trigger a console warning in development mode (SCRIPT_DEBUG). Direct access will stop working in a future version of WordPress. An official mechanism for tracking navigation state will be introduced in WordPress 7.1.

See #70882 for more details.

Props to @yashjawale for the implementation.

state.url from core/router is now populated on the server

Previously, state.url in the core/router store was initialized on the client by setting it to window.location.href. This meant the value was undefined until the @wordpress/interactivity-router module finished loading asynchronously, requiring developers to guard against that initial undefined state and filterFilter Filters are one of the two types of Hooks https://codex.wordpress.org/Plugin_API/Hooks. They provide a way for functions to modify data of other functions. They are the counterpart to Actions. Unlike Actions, filters are meant to work in an isolated manner, and should never have side effects such as affecting global variables and output. out the subsequent initialization to avoid reacting to it as an actual navigation.

Starting in WordPress 7.0, this value is populated on the server during directive processing, meaning its value doesnโ€™t change until the first client-side navigation occurs.

This makes it possible to combine watch() and state.url to reliably track client-side navigations, for example, to send analytics on each virtual page view:

import { store, watch } from '@wordpress/interactivity';

const { state } = store( 'core/router' );

watch( () => {
    // This runs on every client-side navigation.
    sendAnalyticsPageView( state.url );
} );

See #10944 for more details.

Props to @luisherranz for the implementation.

#7-0, #dev-notes, #dev-notes-7-0, #interactivity-api

DataViews, DataForm, et al. in WordPress 7.0

Previous cycle: WordPress 6.9.

This is a summary of the changes introduced in the โ€œdataviews spaceโ€ during the WordPress 7.0 cycle from the APIAPI An API or Application Programming Interface is a software intermediary that allows programs to interact with each other and share data in limited, clearly defined ways. perspective. They have been posted inย the corresponding iteration issueย as well. To follow whatโ€™s next, subscribe to theย iteration issue for WordPress 7.1.

The changes listed here includeย 166 contributionsย byย 35 unique authorsย across the community during the pastย 4.5 monthsย (since October 17th, 2025).

Continue reading โ†’

#7-0, #dev-notes, #dev-notes-7-0