How to Use a Popover Component

There are many components to choose from when building blocks, and we’ll be covering all of them sooner or later (pinky swear), but today we’ll be exploring how to use the Popover component in your own projects.

This is such an easy-to-use component for whenever you need to display content in a temporary floating ‘popup’ window. It’s similar to a tooltip but can display any type of content, not just text.

Where Do I Start?

We’ll be adding the Popover component to a new block via the @wordpress/create-block package if you want to follow along. You can also add it to any existing block without too much trouble.

Let’s start by creating a new block plugin with the following command. First, make sure you’re in the plugins folder of your local WordPress site (e.g. ./wp-content/plugins/):

npx @wordpress/create-block popover

If you’re not that familiar with creating blocks then I highly recommend checking out my in-depth block creation guide which covers everything you need to know.

Note: Throughout the rest of this article I assume that you’re using the @wordpress/create-block package.

Add the Popover Component

The official documentation recommends adding the Popover as a child component inside a container (usually a Button component), which acts as the trigger to toggle visibility.

The Popover component is located in the @wordpress/components package. Add it to the newly created block in ./src/edit.js next to the other import statements.

import { Popover } from '@wordpress/components';

Now we have access to it let’s add it to our code. There’s a Popover example included in the block editor handbook so we’ll use that:

const MyPopover = () => {
  const [ isVisible, setIsVisible ] = useState( false );
  const toggleVisible = () => {
    setIsVisible( ( state ) => ! state );
  };

  return (
    <Button variant="secondary" onClick={ toggleVisible }>
      Toggle Popover!
      { isVisible && <Popover>Popover is toggled!</Popover> }
    </Button>
  );
};

Add the example MyPopover function component above the existing Edit component.

As this example makes use of the Button component and useState React hook we’ll need to import these too. Just replace the Popover import statement above with the following:

import { Button, Popover } from '@wordpress/components';
import { useState } from '@wordpress/element';

You should now have two React components (MyPopover and Edit), one of which is being exported as the main component for edit.js. However, nothing will change in the block editor output until you add the custom component to Edit.

export default function Edit() {
  return (
    <p {...useBlockProps()}>
      {__( 'Popover – hello from the editor!', 'popover' ) }
      <MyPopover />
    </p>
  );
}

Here’s what it should look like in the block editor.

To make the button slightly more aesthetic comment out the default styles in style.scss and editor.scss apart from the padding, and change the markup structure returned by Edit to:

export default function Edit() {
  return (
    <div {...useBlockProps()}>
      <p>{__( 'Popover – hello from the editor!', 'popover' ) }</p>
      <MyPopover />
    </div>
  );
}

Refresh the block editor and click the ‘Toggle Popover’ button to display the Popover. To close it, either click the toggle button again, or anywhere inside the Popover content.

How Does It Work?

Here’s what you should have in your Edit component. Let’s walk through the code to make sure everything is clear.

import { __ } from '@wordpress/i18n';
import { useBlockProps } from '@wordpress/block-editor';
import { Button, Popover } from '@wordpress/components';
import { useState } from '@wordpress/element';
import './editor.scss';

const MyPopover = () => {
  const [ isVisible, setIsVisible ] = useState( false );
  const toggleVisible = () => {
    setIsVisible( ( state ) => ! state );
  };

  return (
    <Button variant="primary" onClick={ toggleVisible }>
      Toggle Popover!
      { isVisible && <Popover>Popover is toggled!</Popover> }
    </Button>
  );
};

export default function Edit() {
  return (
    <div {...useBlockProps()}>
      <p>{__( 'Popover – hello from the editor!', 'popover' ) }</p>
      <MyPopover />
    </div>
  );
}

After the required components and styles are imported, we define a custom MyPopover function component that acts as a wrapper for the actual Popover. This is mainly for convenience and maintainability so we can implement all associated Popover data and functionality inside a single custom component. It’s a common pattern for components, which you’ll come across again and again.

The Popover component is added inside a Button component which has an associated onClick callback to toggle visibility.

Because we’re using a React function component, state is implemented via the useState React hook. If you’re not that familiar with React hooks I’d recommend reading up on them if you get the chance. Don’t worry, we’ll cover them in much more depth in future articles.

For now though all you need to understand is that our Popover state is stored in isVisible and is updated via the setIsVisible function. Whenever the ‘Toggle Popover!” button is clicked the toggleVisible callback invokes setIsVisible to modify the isVisible state value.

Top Tip! Don’t ever be tempted to update React hook state values directly. Instead, always update them via their associated modifier function. e.g. In our case, to update isVisible we use setIsVisible( ( state ) => ! state ).

Sample Use Case for the Popover Component

So far our Popover only displays text content but in practice you’ll want to add something a little more useful. As an example, how about using it to display a formatted table of block props?

We could implement this directly inside the Popover component but it’s good practice to separate out new functionality like this into it’s own custom component.

Add a second custom component to Edit called DisplayComponentProps. For now this is just comprised of a heading and an empty table:

const DisplayComponentProps = (props) => {
  return (
    <div style={{ padding: "15px" }}>
      <h2>Block Props</h2>
      <table className="component-props-table">
      </table>
    </div>
  );
}

Replace the ‘Toggle Popover!’ text with the new DisplayComponentProps custom component:

return (
  <Button variant="primary" onClick={toggleVisible}>
    Toggle Popover!
    {isVisible &&
      <Popover>
        <DisplayComponentProps />
      </Popover>
    }
  </Button>
);

While we’re at it let’s add some CSS to editor.scss to style the DisplayComponentProps component:

.component-props-table {
  width: 400px;
  font-family: Arial, Helvetica, sans-serif;
  border-collapse: collapse;

  td, th {
    border: 1px solid #ddd;
    padding: 12px;
  }

  td:first-child {
    text-align: center;
  }
  tr:nth-child(even){background-color: #f2f2f2;}

  th {
    padding-top: 12px;
    padding-bottom: 12px;
    text-align: left;
    background-color: #04AA6D;
    color: white;
  }
}

.popover-settings-row {
  justify-content: end;
}

The block props we want to display are passed into our block at the top level via the Edit component. They then need passing to MyPopover and then in turn to DisplayComponentProps so that they can be accessed and formatted for display. How do we do this exactly?

Start by passing in props to the Edit, MyPopover, and DisplayComponentProps components.

const MyPopover = (props) => {
  // ...
};

const DisplayComponentProps = (props) => {
  // ...
}

export default function Edit(props) {
  // ...
}

Then, pass the block props to MyPopover from Edit:

// Edit component
<MyPopover blockProps={props} />

And finally pass the props to DisplayComponentProps from MyPopover:

// MyPopover component
<DisplayComponentProps blockProps={blockProps} />

Once the block props are accessible in DisplayComponentProps we can quickly output them to make sure we’re on the right track.

{JSON.stringify(blockProps)}

We’re almost there now. The last step is to iterate over the block props object, outputting each key/value pair as a new table row.

<table className="component-props-table">
  <thead><tr><th>Index</th><th>Key</th><th>Value</th></tr></thead>
    {Object.entries(blockProps).map(([key, value], index) => {
      return (
        <tr><td>{index}</td><td>{key}</td><td>{JSON.stringify(value)}</td></tr>
      );
    })}
</table>

Object.entries is used to generate an iterable array of items as, by default, Array.prototype.map() can’t iterate over objects directly.

Remove the stringified object and display the Popover once again. We should now see a nice formatted table of block props.

Here’s how edit.js is shaping up now:

import { __ } from '@wordpress/i18n';
import { useBlockProps } from '@wordpress/block-editor';
import { Button, Popover } from '@wordpress/components';
import { useState } from '@wordpress/element';
import './editor.scss';

const MyPopover = (props) => {
  const { blockProps } = props;
  const [isVisible, setIsVisible] = useState(false);
  const toggleVisible = () => {
    setIsVisible((state) => !state);
  };

  return (
    <Button variant="primary" onClick={toggleVisible}>
      Toggle Popover!
      {isVisible &&
        <Popover>
          <DisplayComponentProps blockProps={blockProps} />
        </Popover>
      }
    </Button>
  );
};

const DisplayComponentProps = (props) => {

  const { blockProps } = props;

  console.log(Object.entries(blockProps));
  return (
    <div style={{ padding: "15px" }}>
      <h2>Block Props</h2>
      <table className="component-props-table">
        <thead><tr><th>Index</th><th>Key</th><th>Value</th></tr></thead>
          {Object.entries(blockProps).map(([key, value], index) => {
            return (
              <tr><td>{index}</td><td>{key}</td><td>{JSON.stringify(value)}</td></tr>
            );
          })}
      </table>
    </div>
  );
}

export default function Edit(props) {
  return (
    <div {...useBlockProps()}>
      <p>{__('Popover – hello from the editor!', 'popover')}</p>
      <MyPopover blockProps={props} />
    </div>
  );
}

Making the Button Label Dynamic

One thing we can do to improve the user experience is to update the button label whenever it’s clicked, to indicate the current Popver status.

So far we only have one state variable (in MyPopover) to keep track of the Popover visibility. Let’s add another one to store the dynamic button label too.

// MyPopover component
const buttonLabels = { display: "Display Props", hide: "Hide Props"};
const [buttonLabel, setButtonLabel] = useState(buttonLabels.display);

Notice how we’ve added the two possible label values to an object and used it to set the default value for the button label state.

We can then update the button text when the Popover visibility changes.

// MyPopover component
const toggleVisible = () => {
  setIsVisible((state) => !state);
  setButtonLabel(isVisible ? buttonLabels.display : buttonLabels.hide);
};

And in MyPopover we can just replace the fixed button label with our buttonLabel state variable.

// MyPopover component
  return (
    <Button variant="secondary" onClick={toggleVisible}>
      {buttonLabel}
      {isVisible &&
        <Popover>
          <DisplayComponentProps blockProps={blockProps} />
        </Popover>
      }
    </Button>
  );
};

Our final MyPopover component is now:

const MyPopover = (props) => {
  const { blockProps } = props;
  const buttonLabels = { display: "Display Props", hide: "Hide Props"};
  const [isVisible, setIsVisible] = useState(false);
  const [buttonLabel, setButtonLabel] = useState(buttonLabels.display);
  const toggleVisible = () => {
    setIsVisible((state) => !state);
    setButtonLabel(isVisible ? buttonLabels.display : buttonLabels.hide);
  };

  return (
    <Button variant="secondary" onClick={toggleVisible}>
      {buttonLabel}
      {isVisible &&
        <Popover>
          <DisplayComponentProps blockProps={blockProps} />
        </Popover>
      }
    </Button>
  );
};

Let’s see how it works now.

Looking good! Now when the Popover toggle button is clicked the label automatically updates to reflect the current visibility status.

Location, Location, Location

The trigger button for our Popover isn’t just limited to being located in the main block content. The block editor is so flexible we can also add it to the block toolbar and settings sidebar with minimal effort.

Each block includes support for its own settings panel, and block toolbar (which no other blocks have access to). You can use these additional locations to insert settings that affect block behavior and control how it renders in the browser.

Let’s take a look at how you can implement both of these editor locations.

Settings Sidebar

The settings sidebar also supports the use of collapsible panels to make managing multiple controls easier. In our case, we’ll just need one panel, containing the Popover button.

Update the Edit import statements to include the new InspectorControls, PanelBody, PanelRow components needed for the settings sidebar:

import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
import { Button, Popover, PanelBody, PanelRow } from '@wordpress/components';

We can then use them to wrap a new instance of the MyPopover component in Edit. Add the following code directly beneath the existing div:

<InspectorControls>
  <PanelBody
    title={__('Block Props', 'popover')}
    initialOpen={true}
  >
    <PanelRow className="popover-settings-row">
      <MyPopover componentProps={props} />
    </PanelRow>
  </PanelBody>
</InspectorControls>

We now have two top-level components which is not allowed in React so make sure to use Fragments to help get around this without introducing extra HTML markup in the block output.

Notice the CSS class added to the PanelRow which helps position the Popover button to the right (see the editor styles we added earlier). Also, PanelBody is set to open by default for convenience. Otherwise, you’d have to specifically expand it to access the Popover button.

Block ToolBar

Once again, update the import statements to include the new components, this time for the block toolbar.

import { useBlockProps, InspectorControls, BlockControls } from '@wordpress/block-editor';
import { Button, Popover, PanelBody, PanelRow, Toolbar, ToolbarButton } from '@wordpress/components';

Similar to before, we can use the new component (BlockControls) to wrap yet another instance of MyPopover. Add the following code directly beneath the InspectorControls code:

<BlockControls>
  <Toolbar label="My Toolbar">
    <MyPopover toolBar={true} blockProps={props} />
  </Toolbar>
</BlockControls>

Whenever the block is selected we see a new button added to the block toolbar which gives us an alternative way to trigger the, by now very familiar, MyPopover component.

You may see the following JavaScript console warning: Using custom components as toolbar controls is deprecated since version 5.6. Please use ToolbarItem, ToolbarButton or ToolbarDropdownMenu components instead.

So in practice, you should probably use the ToolbarButton component now when adding controls to the block toolbar. But, for our example using a standard button is fine for demonstration purposes.

If you’re interested in how you might get around this issue with MyPopover, one way would be to conditionally output a Button or ToolbarButton component depending on the props passed in. e.g. For the block toolbar MyPopover component, you could pass in a new toolBar prop:

<MyPopover toolBar={true} blockProps={props} />

And in MyPopover extract this prop into a const, which defaults to false if the toolBar prop is omitted.

const { blockProps, toolBar = false } = props;

You could then use this value to conditionally wrap the Popover component with Button or ToolBarButton.

Wrapping Up

We now have three places inside the block editor that we can trigger our Popover from!

In each location we’re reusing the same custom MyPopover component. This gives you an idea of just how flexible and straightforward it is to use React to add functionality to various locations around the editor.

In general, you can choose which block controls are used in each editor location that best suits your particular use case. Some controls can be used in more than one location, as demonstrated in our example.

Here’s the final ./src/edit.js code:

import { __ } from '@wordpress/i18n';
import { useBlockProps, InspectorControls, BlockControls } from '@wordpress/block-editor';
import { Button, Popover, PanelBody, PanelRow, Toolbar, ToolbarButton } from '@wordpress/components';
import { useState } from '@wordpress/element';
import './editor.scss';

const MyPopover = (props) => {
  const { blockProps } = props;
  const buttonLabels = { display: "Display Props", hide: "Hide Props" };
  const [isVisible, setIsVisible] = useState(false);
  const [buttonLabel, setButtonLabel] = useState(buttonLabels.display);
    const toggleVisible = () => {
    setIsVisible((state) => !state);
    setButtonLabel(isVisible ? buttonLabels.display : buttonLabels.hide);
  };

  return (
    <Button variant="secondary" onClick={toggleVisible}>
      {buttonLabel}
      {isVisible &&
        <Popover>
          <DisplayComponentProps blockProps={blockProps} />
        </Popover>
      }
    </Button>
  );
};

const DisplayComponentProps = (props) => {

  const { blockProps } = props;

  console.log(Object.entries(blockProps));
  return (
    <div style={{ padding: "15px" }}>
      <h2>Block Props</h2>
      <table className="component-props-table">
        <thead><tr><th>Index</th><th>Key</th><th>Value</th></tr></thead>
        {Object.entries(blockProps).map(([key, value], index) => {
          return (
            <tr><td>{index}</td><td>{key}</td><td>{JSON.stringify(value)}</td></tr>
          );
        })}
      </table>
    </div>
  );
}

export default function Edit(props) {
  return (
    <>
      <div {...useBlockProps()}>
        <p>{__('Popover – hello from the editor!', 'popover')}</p>
        <MyPopover blockProps={props} />
      </div>
      <InspectorControls>
        <PanelBody
          title={__('Component Props', 'popover')}
          initialOpen={true}
        >
          <PanelRow className="popover-settings-row">
            <MyPopover blockProps={props} />
          </PanelRow>
        </PanelBody>
      </InspectorControls>
      <BlockControls>
        <Toolbar label="My Toolbar">
          <MyPopover blockProps={props} />
        </Toolbar>
      </BlockControls>
    </>
  );
}

InnerBlock Thoughts

In this article, we’ve covered the basics of how to use the Popover component to display arbitrary content.

Hopefully, you’ll find the example MyPopover custom component useful in your own projects when you need to know what block props are available. Incidentally, you could also use it to examine the props for any React component, not just the block props, as MyPopover is generic enough to handle any props object.

The Popover component is pretty versatile and can display whatever content you like. However, if you need to interact with Popover content then a better choice might be to use the new Flyout component. This is still currently an experimental feature (at the time of writing) so it’s not recommended for use in production just yet.

Leave a Comment

Let's Learn Block Development Together!

If you want to learn all about block editor development then please signup to the newsletter below. I've got some awesome content planned which I can't wait to share with you!