---
title: Editor Tabs
description: MediaPress Editor Tabs enables a tabbed view of the block editor interface, allowing other packages or code to render custom tabs in the top toolbar.
url: https://developers.wpengine.com/docs/newsroom/mediapress/editor-tabs/
image: https://developers.wpengine.com/og/docs/newsroom/mediapress/editor-tabs.png
---

# Editor Tabs

MediaPress Editor Tabs enables a tabbed view of the block editor interface, by allowing other packages or code to render custom tabs in a central location within the top toolbar of the block editor.

Whilst this package handles the rendering of the tab selection UI, this is essentially just a group of buttons. It is up to external code to handle how content is rendered when each button is clicked, and the conditions under which the tab/button is deemed to be ‘active’.

> **Tip**
>
> For a fully-featured tabbed interface, it is recommended to register a new full-screen block editor sidebar using `Panel` from our `components` package. This method will also ensure that you have a way to access your panel if the Editor Tabs are not enabled, rather than introducing a dependency on this package.

## Additional Configuration

Editor Tabs must be activated via the “MediaPress” settings page. Initially, it will render a single tab for the “Post” view, but other packages or code may filter this to introduce additional tabs.

## Filters/Actions

### PHP

#### Filters

##### `mediapress_editor_tabs_supported_post_types`

Defines the post types on which the editor tabs functionality will be enabled.

**Default Value**

`[ 'post', 'page' ]`

**Parameters**

| Name       | Type  | Description              |
| ---------- | ----- | ------------------------ |
| post_types | array | Array of post type slugs |

**Usage**

```php
add_filter( 'mediapress_editor_tabs_supported_post_types', 'my_plugin_add_editor_tabs_support' );
function my_plugin_add_editor_tabs_support( array $post_types ): array {
  $post_types[] = 'my_custom_post_type';
  return $post_types;
}
```

### JavaScript

#### Filters

##### `mediaPress.editorTabs.tabs`

Allows filtering of the tab buttons shown in the block editor toolbar when this package is enabled.

Each tab is a button which can be provided with a click handler, as well state to determine whether that tab is ‘active’ or not.

**Parameters**

| Name                | Type      | Description                                                                                                                                                                                |
| ------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| tabs                | Object    | Object containing individual tab objects, keyed by the tab name                                                                                                                            |
| tabs.key            | Object    | Individual tab object                                                                                                                                                                      |
| tabs.key.label      | args.type | The user-facing label to show within the tab button                                                                                                                                        |
| tabs.key.onClick    | Function  | Callback to run when the tab is clicked by a user. Not run directly while the tab renders as a dropdown, but reachable from a sub-menu fill through `activate` — see “Tab sub-menus” below |
| tabs.key.isActive   | boolean   | Expected to contain conditions to determine whether the tab is currently active or not                                                                                                     |
| tabs.key.order      | number    | The priority order in which this tab should appear in the list                                                                                                                             |
| tabs.key.hasSubMenu | boolean   | Render the tab as a dropdown rather than a plain button. See “Tab sub-menus” below                                                                                                         |

**Usage**

```js
addFilter('mediaPress.editorTabs.tabs', 'my-plugin/add-media-tab', (tabs) => {
  return {
    ...tabs,
    meta: {
      label: __('Media', 'default'),
      onClick: () =>
        dispatch(editPostStore).openGeneralSidebar(MEDIA_SIDEBAR_NAME),
      isActive:
        select(editPostStore).getActiveGeneralSidebarName() ===
        MEDIA_SIDEBAR_NAME,
      order: 2,
    },
  };
});
```

## Tab sub-menus

Any tab can render as a dropdown instead of a plain button — the tab this package provides itself, and any tab added through the filter. This takes two steps, because the tab filter carries data only and cannot express arbitrary markup:

1. Set `hasSubMenu: true` on the tab through the `mediaPress.editorTabs.tabs` filter. A tab rendering as a dropdown never runs its own `onClick` from clicking the tab itself, open or closed; clicking it opens the menu instead. The `onClick` is still reachable from inside the menu through `activate`, described under “Returning to the tab’s own destination” below, and is run directly only if the tab falls back to a plain button, described further down.
2. Render the menu items into the tab’s slot.

The slot name is `MediaPressEditorTab/<tab key>`, and is part of this package’s public API. Fill it with `Fill` from `@wordpress/components`, using the children-as-function form to receive the fill props — `onClose` here, `activate` covered under “Returning to the tab’s own destination”, and `ContextMenu` covered in “Context menus”:

```js
import { Fill, MenuGroup, MenuItem } from '@wordpress/components';


<Fill name="MediaPressEditorTab/channels">
  {({ onClose }) => (
    <MenuGroup label={__('My things', 'my-plugin')}>
      <MenuItem
        onClick={() => {
          doSomething();
          onClose();
        }}
      >
        {__('My item', 'my-plugin')}
      </MenuItem>
    </MenuGroup>
  )}
</Fill>;
```

Both steps work the same way whichever tab you are addressing, so there are two things you can do with them. A **new tab** can be created as a sub-menu, by adding it through the filter with `hasSubMenu` set and filling its slot. An **existing tab** can be transformed into one, by spreading it and adding `hasSubMenu` — including a tab belonging to another module, or the `editor` tab this package provides:

```js
addFilter(
  'mediaPress.editorTabs.tabs',
  'my-plugin/add-channels-tab',
  (tabs) => ({
    ...tabs,


    // A new tab, created as a sub-menu.
    channels: {
      label: __('Channels', 'my-plugin'),
      hasSubMenu: true,
      onClick: openChannelsPanel,
      isActive: isChannelsPanelOpen,
      order: 20,
    },


    // An existing tab, transformed into one.
    editor: { ...tabs.editor, hasSubMenu: true },
  }),
);
```

The slot is rendered with `bubblesVirtually`, so a fill stays inside the React tree of the module that declares it. Your menu items keep their own context providers and hooks, even though the markup ends up inside the dropdown. This also means the slot name is the only coupling: a module in a separate plugin can fill it without importing anything from this package.

Any number of modules may fill the same tab; their items appear in fill registration order.

### Grouping items

Nothing is rendered around the slot, so a fill decides its own grouping. Wrap your items in a `MenuGroup` with a `label`, as above: the label becomes a heading over the group and its accessible name, which tells a user what that part of the menu is for.

This is worth doing even for a single group, because a tab is shared. When two modules fill the same tab, each brings its own `MenuGroup`, so their items are headed separately and divided from one another instead of running together in one undifferentiated list. A module cannot know what else is in the menu alongside it, so it should always say what its own items are.

A fill may render more than one group if its items divide into more than one kind, and may render none at all — bare items are still valid, they simply get no heading and no padding of their own.

> **Caution**
>
> Registering a fill on a tab that has not set `hasSubMenu` renders nothing at all. No slot is ever rendered to receive it, so the items are silently dropped.

### Falling back to a single tab

A tab renders as a dropdown only while at least one module is filling its slot. With no fills it falls back to a plain button and runs its own `onClick`, exactly as a tab that never set `hasSubMenu`.

This matters because the two steps above are usually taken by different modules, and the one declaring the sub-menu cannot see whether anything will fill it — the filling modules are individually toggleable and may live in another plugin. The fallback means you can set `hasSubMenu` unconditionally and let the tab degrade on its own, rather than modelling the presence of modules you cannot see:

```js
channels: {
  label: __('Channels', 'my-plugin'),
  hasSubMenu: true,
  // Used when nothing fills the sub-menu. Worth giving to any tab that sets `hasSubMenu`.
  onClick: openChannelsPanel,
  isActive: isChannelsPanelOpen,
  order: 20,
}
```

A tab that sets `hasSubMenu` and gives no `onClick` becomes an inert button when nothing fills it, so give one to any tab that declares a sub-menu.

> **Note**
>
> The fallback counts fills, not the items they render, since a fill’s contents cannot be inspected without rendering them. A fill that renders no items still counts as a fill, and the tab still shows a sub-menu — an empty one. If your fill can have nothing to show, render no `Fill` at all in that case rather than an empty one.

### Returning to the tab’s own destination

A dropdown tab’s own `onClick` stops being reachable from the tab itself once it has at least one fill — clicking the tab only opens the menu. That leaves nothing to take a user back to whatever the tab represents, if one of the items in its own menu is “the thing this tab is already showing”.

`activate`, given to every fill alongside `onClose`, runs that `onClick` on request. It is a no-op while the tab is already `isActive`, so it is safe to call without checking that yourself:

```js
<Fill name="MediaPressEditorTab/channels">
  {({ onClose, activate }) => (
    <MenuGroup label={__('Channels', 'my-plugin')}>
      <MenuItem
        role="menuitemcheckbox"
        isSelected={isOnThisTabsOwnDestination}
        onClick={() => {
          activate();
          onClose();
        }}
      >
        {__('My destination', 'my-plugin')}
      </MenuItem>
    </MenuGroup>
  )}
</Fill>
```

This needs no extra property on the tab: `onClick` and `isActive` are already required by every tab, including ones that set `hasSubMenu` (see “Falling back to a single tab” above), so `activate` is built from state the tab already carries.

> **Caution**
>
> Call `activate` only from the item that represents the tab’s own destination, not unconditionally from every item. Most items in a sub-menu do something else entirely — navigate elsewhere, enable something, open a different view — and calling `activate` anyway would trigger the tab’s own side effects (a sidebar switch, say) alongside whatever that item already does, for no reason.

## Context menus

An item in a sub-menu can carry its own menu of secondary actions, opened from a trigger at the end of the row. This lets a user act on something the item represents without having to navigate to it first.

The `ContextMenu` component that provides this arrives through the slot’s fill props, in the same children-as-function form as `onClose`. It is not exported, so nothing needs to be imported and a module in a separate plugin can use it on exactly the same terms as one in this repository.

Wrap the item you want the menu on, and describe the actions with `controls`:

```js
import { Fill, MenuGroup, MenuItem } from '@wordpress/components';
import { trash, unlink } from '@wordpress/icons';


<Fill name="MediaPressEditorTab/channels">
  {({ onClose, ContextMenu }) => (
    <MenuGroup label={__('Channels', 'my-plugin')}>
      <ContextMenu
        label={__('Apple News actions', 'my-plugin')}
        controls={[
          {
            title: __('Delete', 'my-plugin'),
            icon: trash,
            onClick: () => deleteChannel(),
          },
          {
            title: __('Decouple', 'my-plugin'),
            icon: unlink,
            onClick: () => decoupleChannel(),
          },
        ]}
      >
        <MenuItem
          onClick={() => {
            navigateToChannel();
            onClose();
          }}
        >
          {__('Apple News', 'my-plugin')}
        </MenuItem>
      </ContextMenu>
    </MenuGroup>
  )}
</Fill>;
```

**Props**

| Name     | Type        | Description                                                                                                                                                                                        |
| -------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| children | JSX.Element | The item the context menu belongs to                                                                                                                                                               |
| controls | Array       | The actions to offer, in the [`controls`](https://developer.wordpress.org/block-editor/reference-guides/components/dropdown-menu/) form used by `DropdownMenu`. Any number of actions may be given |
| label    | string      | Accessible name for the trigger. Include what the actions apply to, since a menu may hold several triggers                                                                                         |

Every action is defined by the module supplying the item, and its `onClick` runs in that module’s own React tree, so it keeps whatever context and hooks the fill has around it.

Passing no `controls`, or an empty array, renders the item exactly as it was given with no trigger. Items in the same sub-menu may therefore mix freely — you can wrap each one unconditionally and let those with actions be the only ones that grow a trigger.

The sub-menu stays open behind an open context menu, so an action can be run and the menu returned to without reopening the tab. It still closes on escape, on a click elsewhere in the editor, and on its tab being pressed again.

> **Note**
>
> The trigger is a sibling of the item, not part of it — a menu item is itself a button, and a button cannot contain another one. Clicking the trigger therefore never runs the item’s own `onClick`.

## Was this helpful?

Thanks for the feedback!

```json
{"@context":"https://schema.org","@graph":[{"@type":"WebSite","@id":"https://developers.wpengine.com/#website","name":"WP Engine Product Documentation","url":"https://developers.wpengine.com/","description":"Resources for developing, deploying, and scaling on WP Engine.","inLanguage":"en-US","keywords":["API documentation","product documentation","headless WordPress","managed WordPress hosting"],"publisher":{"@id":"https://wpengine.com/#organization"}}]}
```
