diff --git a/packages/core-data/src/hooks/use-post-editor-awareness-state.ts b/packages/core-data/src/hooks/use-post-editor-awareness-state.ts
index 5acd74939313a9..3b64ade4cd0211 100644
--- a/packages/core-data/src/hooks/use-post-editor-awareness-state.ts
+++ b/packages/core-data/src/hooks/use-post-editor-awareness-state.ts
@@ -96,7 +96,7 @@ function usePostEditorAwarenessState(
* @param postType - The type of the post.
* @return {ActiveUser[]} The active users.
*/
-export function useActiveUsers(
+export function useActiveCollaborators(
postId: number | null,
postType: string | null
): ActiveUser[] {
diff --git a/packages/core-data/src/private-apis.js b/packages/core-data/src/private-apis.js
index e9fcf36f7e6090..917af885a9b8cd 100644
--- a/packages/core-data/src/private-apis.js
+++ b/packages/core-data/src/private-apis.js
@@ -3,10 +3,12 @@
*/
import { useEntityRecordsWithPermissions } from './hooks/use-entity-records';
import { RECEIVE_INTERMEDIATE_RESULTS } from './utils';
+import { useActiveCollaborators } from './hooks/use-post-editor-awareness-state';
import { lock } from './lock-unlock';
export const privateApis = {};
lock( privateApis, {
useEntityRecordsWithPermissions,
RECEIVE_INTERMEDIATE_RESULTS,
+ useActiveCollaborators,
} );
diff --git a/packages/editor/src/components/collaborators-presence/avatar.tsx b/packages/editor/src/components/collaborators-presence/avatar.tsx
new file mode 100644
index 00000000000000..73cfe7ade43d0f
--- /dev/null
+++ b/packages/editor/src/components/collaborators-presence/avatar.tsx
@@ -0,0 +1,50 @@
+import { clsx } from 'clsx';
+import { type PostEditorAwarenessState } from '../../../../core-data/src/awareness/types';
+import './styles/avatar.scss';
+
+type AvatarSize = 'small' | 'medium';
+
+interface AvatarProps {
+ userInfo: PostEditorAwarenessState[ 'userInfo' ];
+ showUserColorBorder?: boolean;
+ size?: AvatarSize;
+}
+
+/**
+ * Renders a circular avatar bubble for a user with an optional border.
+ *
+ * @param {Object} props Component props.
+ * @param {Object} props.userInfo User information.
+ * @param {boolean} props.showUserColorBorder Whether to show the user color border.
+ * @param {string} props.size Size of the avatar.
+ */
+export function Avatar( {
+ userInfo,
+ showUserColorBorder,
+ size = 'small',
+}: AvatarProps ) {
+ const className = clsx(
+ 'editor-collaborators-presence__avatar',
+ `editor-collaborators-presence__avatar--${ size }`,
+ showUserColorBorder &&
+ 'editor-collaborators-presence__avatar--with-color-border'
+ );
+
+ const avatarUrl =
+ userInfo.avatar_urls?.[ 48 ] ||
+ userInfo.avatar_urls?.[ 96 ] ||
+ userInfo.avatar_urls?.[ 24 ];
+
+ const avatarStyles = {
+ '--avatar-url': `url(${ avatarUrl })`,
+ '--user-color': userInfo.color,
+ } as React.CSSProperties;
+
+ return (
+
+ );
+}
diff --git a/packages/editor/src/components/collaborators-presence/index.tsx b/packages/editor/src/components/collaborators-presence/index.tsx
new file mode 100644
index 00000000000000..334ccb415ea682
--- /dev/null
+++ b/packages/editor/src/components/collaborators-presence/index.tsx
@@ -0,0 +1,94 @@
+import { Button } from '@wordpress/components';
+import { useState } from '@wordpress/element';
+import { privateApis } from '@wordpress/core-data';
+
+import { Avatar } from './avatar';
+import { CollaboratorsList } from './list';
+import { unlock } from '../../lock-unlock';
+import { type PostEditorAwarenessState } from '../../../../core-data/src/awareness/types';
+
+import './styles/collaborators-presence.scss';
+
+const { useActiveCollaborators } = unlock( privateApis );
+
+interface CollaboratorsPresenceProps {
+ postId: number | null;
+ postType: string | null;
+}
+
+/**
+ * Renders a list of avatars for the active users, with a maximum of 3 visible avatars.
+ * Shows a popover with all users on hover.
+ *
+ * @param {Object} props CollaboratorsPresence component props
+ * @param {number} props.postId ID of the post
+ * @param {string} props.postType Type of the post
+ */
+export function CollaboratorsPresence( {
+ postId,
+ postType,
+}: CollaboratorsPresenceProps ) {
+ const activeUsers = useActiveCollaborators(
+ postId,
+ postType
+ ) as PostEditorAwarenessState[];
+
+ // Filter out current user - we never show ourselves in the list
+ const otherActiveUsers = activeUsers.filter( ( user ) => ! user.isMe );
+
+ const [ isPopoverVisible, setIsPopoverVisible ] = useState( false );
+ const [ popoverAnchor, setPopoverAnchor ] = useState< HTMLElement | null >(
+ null
+ );
+
+ // When there are no other collaborators, this component should not render
+ // at all. This will always be the case when collaboration is not enabled, but
+ // also when the current user is the only editor with the post open.
+ if ( otherActiveUsers.length === 0 ) {
+ return null;
+ }
+
+ const visibleUsers = otherActiveUsers.slice( 0, 3 );
+ const remainingUsers = otherActiveUsers.slice( 3 );
+ const remainingUsersText = remainingUsers
+ .map( ( { userInfo } ) => userInfo.name )
+ .join( ', ' );
+
+ return visibleUsers.length > 0 ? (
+
+
+ { isPopoverVisible && (
+
+ ) }
+
+ ) : null;
+}
diff --git a/packages/editor/src/components/collaborators-presence/list.tsx b/packages/editor/src/components/collaborators-presence/list.tsx
new file mode 100644
index 00000000000000..4040f94099ca61
--- /dev/null
+++ b/packages/editor/src/components/collaborators-presence/list.tsx
@@ -0,0 +1,79 @@
+import { __ } from '@wordpress/i18n';
+import { Popover, Button } from '@wordpress/components';
+import { close } from '@wordpress/icons';
+
+import { Avatar } from './avatar';
+
+import './styles/collaborators-list.scss';
+import { type PostEditorAwarenessState } from '../../../../core-data/src/awareness/types';
+
+interface CollaboratorsListProps {
+ activeUsers: PostEditorAwarenessState[];
+ popoverAnchor?: HTMLElement | null;
+ setIsPopoverVisible: ( isVisible: boolean ) => void;
+}
+
+/**
+ * Renders a list showing all active collaborators with their details.
+ * Note: activeUsers should already exclude the current user (filtered by parent component).
+ * @param props Component props
+ * @param props.activeUsers List of active users
+ * @param props.popoverAnchor Anchor element for the popover
+ * @param props.setIsPopoverVisible Callback to set the visibility of the popover
+ */
+export function CollaboratorsList( {
+ activeUsers,
+ popoverAnchor,
+ setIsPopoverVisible,
+}: CollaboratorsListProps ) {
+ return (
+ setIsPopoverVisible( false ) }
+ >
+