If you’ve ever felt that most WordPress admin areas seem static and outdated compared to the modern and dynamic Post and Site Editor, you’re not alone.
But there’s good news: this is changing. With WordPress entering Phase 3: Collaboration, there’s an ongoing effort to improve the overall admin experience and integrate the new visual language throughout.
The WordPress ecosystem is changing, too. Numerous well-known plugins have had their settings pages updated. More and more developers choose React for their user interfaces to enhance user experience and speed up the introduction of new features.
What if you want to do the same?
This article will guide you through creating a React-based settings page for a plugin utilizing WordPress React components.
Table of Contents
What are you going to build?
For this article, you’ll build a plugin that displays an announcement bar above the header on the front end.
It will include a settings page allowing you to customize the message, toggle the bar’s visibility, and adjust its size.
Here’s a sneak peek at the end result:
The power of modular architecture
Engaging with the Post and Site Editor gives the impression of a unified system. However, they are the product of the orchestration of independent components and packages.
From the beginning, the Gutenberg project embraced having loosely coupled or completely decoupled packages. With this modular architecture, it’s possible to create different interfaces or even applications.
You will use these packages to create a settings page, but given their reusability and versatility, you could use them even for non-WordPress-based projects.
Assumed knowledge and prerequisites
This article assumes you have some familiarity with React. It will be particularly accessible if you have ever developed a basic custom block, as the skills and concepts are highly transferable.
If you want to follow along with coding, create the following starter files:
/plugins/unadorned-announcement-bar/
├── index.php
├── package.json
└── src/
├── index.js
└── index.scss
You’ll also need wp-scripts
for the JavaScript build step. For a step-by-step walkthrough on setting up and using it, check out the Get started with wp-scripts.
If you are already comfortable with it, here’s what you need in the package.json
to get started:
{
"devDependencies": {
"@wordpress/scripts": "^27.0.0"
},
"scripts": {
"build": "wp-scripts build",
"start": "wp-scripts start"
}
}
Laying the groundwork
The first steps do not differ from creating a plugin with a “traditional” custom settings page.
This section is kept short and to the point. If you require more information on any of the steps, you can refer to the Plugin Basics pages:
Setting up the menu and settings page
First, to get started, add the minimally required plugin header comment to index.php
file, then register a sub-menu under the Settings menu:
<?php
/**
* Plugin Name: Unadorned Announcement Bar
*/
function unadorned_announcement_bar_settings_page() {
add_options_page(
__( 'Unadorned Announcement Bar', 'unadorned-announcement-bar' ),
__( 'Unadorned Announcement Bar', 'unadorned-announcement-bar' ),
'manage_options',
'unadorned-announcement-bar',
'unadorned_announcement_bar_settings_page_html'
);
}
add_action( 'admin_menu', 'unadorned_announcement_bar_settings_page' );
Next, add the callback function to the same PHP file:
function unadorned_announcement_bar_settings_page_html() {
printf(
'<div class="wrap" id="unadorned-announcement-bar-settings">%s</div>',
esc_html__( 'Loading…', 'unadorned-announcement-bar' )
);
}
It’s enough to output an HTML element with a unique id. You’ll use this id
to target the element in JavaScript. The “Loading…” message will be replaced once the JavaScript part is initialized.
To access your settings page, go to the Settings and then Unadorned Announcement Bar:
Enqueuing the JavaScript for React
Given that you are building a React-based settings page, you must enqueue the necessary JavaScript file. To do this, add the following code to the index.php
file:
function unadorned_announcement_bar_settings_page_enqueue_style_script( $admin_page ) {
if ( 'settings_page_unadorned-announcement-bar' !== $admin_page ) {
return;
}
$asset_file = plugin_dir_path( __FILE__ ) . 'build/index.asset.php';
if ( ! file_exists( $asset_file ) ) {
return;
}
$asset = include $asset_file;
wp_enqueue_script(
'unadorned-announcement-bar-script',
plugins_url( 'build/index.js', __FILE__ ),
$asset['dependencies'],
$asset['version'],
array(
'in_footer' => true,
)
);
}
add_action( 'admin_enqueue_scripts', 'unadorned_announcement_bar_settings_page_enqueue_style_script' );
You are following best practices by ensuring the JavaScript is only loaded on the settings page, and since you are setting the dependencies and the version dynamically, you’ll never have to update them.
The build folder and its contents are created when the JavaScript is compiled with wp-scripts
.
Kick-starting the React settings page
With the previous steps out of the way, it’s finally time to write some React code.
To confirm everything, you have done works, start with something basic, like rendering a placeholder component.
Add this code to your src/index.js
file:
import domReady from '@wordpress/dom-ready';
import { createRoot } from '@wordpress/element';
const SettingsPage = () => {
return <div>Placeholder for settings page</div>;
};
domReady( () => {
const root = createRoot(
document.getElementById( 'unadorned-announcement-bar-settings' )
);
root.render( <SettingsPage /> );
} );
Remember to install the mentioned packages with npm install {package}
. As you encounter references to new packages, install those as well. If you haven’t done so, start the wp-scripts
build process.
By using domReady
, you ensure the DOM is ready for manipulation. Inside the callback, with the createRoot
, you tell React to take over managing the DOM for the specified element. Then, it’s a matter of rendering the SettingsPage
component.
These steps are common to all React applications. Something very similar occurs when the Post and Site Editor are initialized.
If you refresh your settings page, you should see the placeholder message displayed:
Reusable UI elements for WordPress and elsewhere
@wordpress/components
is one of the cornerstone packages. It’s often used, as it contains common UI elements used for the Block and Site Editor, but they are generic enough to be used elsewhere.
See the WordPress Storybook for an overview of the available components. The Storybook allows you to browse individual components, their controls, options, and settings in isolation. You can modify their controls and arguments and see the changes right away.
There is a wide range of components available, and you can expect this list to expand as more areas of WordPress undergo overhauls
In the Storybook, you interact with React code components, which also exist as visual components. These mirror exactly what’s available and implemented in the code.
Visual components
Visual components are reusable bits of UI that can be used in Figma for creating mockups and to explore ideas early on before writing any code.
You can create a mockup for the plugin just by selecting from the available visual components:
If you want to remain consistent with the design of WordPress, it’s a matter of combining and arranging a few controls, along with adding a button and a title:
Building the UI of the settings page
The Panel
Based on the mockup, your settings are grouped into two sections, with the Appearance section being hidden by default.
The Panel
component is a perfect fit for this use case, as it creates a container with an optional header and accepts a PanelBody
component that you can collapse.
To integrate it, you first need to import the dependencies at the top of the src/index.js
file:
import { __ } from '@wordpress/i18n';
import { Panel, PanelBody, PanelRow } from '@wordpress/components';
Remember that the import statements always go at the top of the file. This detail won’t be mentioned explicitly from now on to avoid repetition.
Then, modify your SettingsPage
component. For now, use a placeholder message where the controls will eventually be:
const SettingsPage = () => {
return (
<Panel>
<PanelBody>
<PanelRow>
<div>Placeholder for message control</div>
</PanelRow>
<PanelRow>
<div>Placeholder for display control</div>
</PanelRow>
</PanelBody>
<PanelBody
title={ __( 'Appearance', 'unadorned-announcement-bar' ) }
initialOpen={ false }
>
<PanelRow>
<div>Placeholder for size control</div>
</PanelRow>
</PanelBody>
</Panel>
);
};
The Panel
, PanelBody
, and PanelRow
components are always used together. These and subsequent components offer various options, not all of which are required for this project. You can discover additional possibilities by exploring the Storybook.
At this point, if you refresh your settings page, you should see the components rendered:
Including the styles of the components
Right now, things do not look quite right. This is because, by default, the components are unstyled, and you still need to include their CSS.
You can solve this by enqueueing the wp-components
stylesheet. Amend your current function in the index.php
file:
function unadorned_announcement_bar_settings_page_enqueue_style_script( $admin_page ) {
// ...
wp_enqueue_style( 'wp-components' );
}
Now, your settings page should look WordPressy:
Managing the state of the controls
You don’t need any complicated state management for this plugin. With only three controls and three values to keep track of, you can start with three instances of useState
and some default values.
It’s best to encapsulate it in a custom hook to separate the state from the presentation layer. From here, until it’s mentioned otherwise, add all code to the src/index.js
file:
import { useState } from '@wordpress/element';
const useSettings = () => {
const [ message, setMessage ] = useState('Hello, World!');
const [ display, setDisplay ] = useState(true);
const [ size, setSize ] = useState('medium');
return {
message,
setMessage,
display,
setDisplay,
size,
setSize,
};
};
While your code is currently just a few lines, it will inevitably grow in complexity once you attempt to save or load data from the database. The benefit of the custom hook and having a central place for state management will become more apparent very soon. Just stay tuned.
To access the state variables and setter functions in the SettingsPage
component, call the useSettings
and destructure the returned object:
const SettingsPage = () => {
const {
message,
setMessage,
display,
setDisplay,
size,
setSize,
} = useSettings();
// ...
};
The message control
To allow for a longer text for the announcement bar message, you can’t go wrong with the TextareaControl
component.
To keep things tidy, wrap it in a separate component:
import { TextareaControl } from '@wordpress/components';
const MessageControl = ( { value, onChange } ) => {
return (
<TextareaControl
label={ __( 'Message', 'unadorned-announcement-bar' ) }
value={ value }
onChange={ onChange }
__nextHasNoMarginBottom
/>
);
};
As WordPress evolves, sometimes it’s necessary to adjust the style of existing components. When new styles are introduced, they are put behind a feature flag prop prefixed by __next
. This offers a grace period for third parties to make any necessary adjustments.
After creating it, the next step is to substitute the placeholder with the MessageControl
component. Use the message
constant as the value and its setter function in your onChange
handler:
const SettingsPage = () => {
const {
message,
setMessage,
// ...
} = useSettings();
return (
<Panel>
<PanelBody>
<PanelRow>
<MessageControl
value={ message }
onChange={ ( value ) => setMessage( value ) }
/>
</PanelRow>
<PanelRow></PanelRow>
</PanelBody>
<PanelBody></PanelBody>
</Panel>
);
};
With all this work, your first control is working, allowing you to update the message:
The display control
To toggle the visibility of the announcement bar, you need an on/off switch. Opt for the ToggleControl
component.
To maintain consistency, follow the same approach as shown in the previous section. First, create a separate component for the control:
import { ToggleControl } from '@wordpress/components';
const DisplayControl = ( { value, onChange } ) => {
return (
<ToggleControl
label={ __( 'Display', 'unadorned-announcement-bar' ) }
checked={ value }
onChange={ onChange }
__nextHasNoMarginBottom
/>
);
};
Then, replace the placeholder you had with the DisplayControl
component, and use the corresponding constant and setter function for the onChange
handler:
const SettingsPage = () => {
const {
// ...
display,
setDisplay,
// ...
} = useSettings();
return (
<Panel>
<PanelBody>
<PanelRow></PanelRow>
<PanelRow>
<DisplayControl
value={ display }
onChange={ ( value ) => setDisplay( value ) }
/>
</PanelRow>
</PanelBody>
<PanelBody></PanelBody>
</Panel>
);
};
The size control
For the size control, you have a couple of different components to choose from, but the best fit is the versatile FontSizePicker
component.
Similar to the previous controls, create a separate component for it. The only difference here is that you must define a list of possible options:
import { FontSizePicker } from '@wordpress/components';
const SizeControl = ( { value, onChange } ) => {
return (
<FontSizePicker
fontSizes={ [
{
name: __( 'Small', 'unadorned-announcement-bar' ),
size: 'small',
slug: 'small',
},
{
name: __( 'Medium', 'unadorned-announcement-bar' ),
size: 'medium',
slug: 'medium',
},
{
name: __( 'Large', 'unadorned-announcement-bar' ),
size: 'large',
slug: 'large',
},
{
name: __( 'Extra Large', 'unadorned-announcement-bar' ),
size: 'x-large',
slug: 'x-large',
},
] }
value={ value }
onChange={ onChange }
disableCustomFontSizes={ true }
__nextHasNoMarginBottom
/>
);
};
By default, you can choose any size outside the list of predefined values, but to keep things “unadorned”, disable this possibility with the disableCustomFontSizes
prop.
For the last time, replace the placeholder text with the actual component and configure the value
and onChange
prop:
const SettingsPage = () => {
const {
// ...
size,
setSize,
} = useSettings();
return (
<Panel>
<PanelBody></PanelBody>
<PanelBody
title={ __( 'Appearance', 'unadorned-announcement-bar' ) }
initialOpen={ false }
>
<PanelRow>
<SizeControl
value={ size }
onChange={ ( value ) => setSize( value ) }
/>
</PanelRow>
</PanelBody>
</Panel>
);
};
At this point, you have all three controls working, which is half of the job. Your settings page should now look like this:
The save button
To complete the basics of your settings page, you need to add two more components.
First and foremost, you need the Button
component for saving. As you did before, create a wrapper component for it:
import { Button } from '@wordpress/components';
const SaveButton = ( { onClick } ) => {
return (
<Button variant="primary" onClick={ onClick } __next40pxDefaultSize>
{ __( 'Save', 'unadorned-announcement-bar' ) }
</Button>
);
};
Next, add the SaveButton
component right after the Panel
component:
const SettingsPage = () => {
// ...
return (
<>
<Panel></Panel>
<SaveButton onClick={ () => {} } />
</>
);
};
When clicked, it’s okay if it doesn’t do anything specific for now. You’ll add the saving functionality in a moment.
Don’t forget to wrap the two components in a fragment (between the <></>
) because you can only return one component in React.
The experimental heading
You might have noticed in the Storybook that some components are marked as experimental.
They are called experimental because WordPress’ backward compatibility guarantee doesn’t apply to them. They can change from one version to another, possibly breaking the code.
Use them with caution, but to try them out, you can use the __experimentalHeading
component for the title instead of a plain h1
tag:
import {
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
__experimentalHeading as Heading,
} from '@wordpress/components';
const SettingsTitle = () => {
return (
<Heading level={ 1 }>
{ __( 'Unadorned Announcement Bar', 'unadorned-announcement-bar' ) }
</Heading>
);
};
If you lint your code with wp-scripts
, that triggers a warning if experimental components are used. You can indicate that you know what you are doing with the eslint-disable-next-line
.
This time, add the component before the Panel
component to render it at the top of the page:
const SettingsPage = () => {
// ...
return (
<>
<SettingsTitle />
<Panel></Panel>
<SaveButton />
</>
);
};
Persisting and loading the settings
On regular WordPress settings pages, when you make a change and save it, you submit a form, and the page is reloaded.
This approach doesn’t align with React’s dynamic interaction model. When something is loaded or saved in Block and Site Editor, that happens seamlessly while many REST API requests are performed in the background. Similarly, you’ll use the REST API to load and save your settings.
You could store the state of the controls as separate single values in the options table, but that would create a new record for each unnecessarily. For this plugin, you should choose the other option: storing them as an array of values under a single key.
Defining the default and schema
To always get back an expected value when you call get_option( 'unadorned_announcement_bar' )
, you should define a default for it.
Also, if you also define the schema strictly, you don’t have to worry about the validation when using the REST API, that’s going to be taken care of by WordPress.
You achieve both using the register_setting
function. Place the following code to your index.php
file:
function unadorned_announcement_bar_settings() {
$default = array(
'message' => __( 'Hello, World!', 'unadorned-announcement-bar' ),
'display' => true,
'size' => 'medium',
);
$schema = array(
'type' => 'object',
'properties' => array(
'message' => array(
'type' => 'string',
),
'display' => array(
'type' => 'boolean',
),
'size' => array(
'type' => 'string',
'enum' => array(
'small',
'medium',
'large',
'x-large',
),
),
),
);
register_setting(
'options',
'unadorned_announcement_bar',
array(
'type' => 'object',
'default' => $default,
'show_in_rest' => array(
'schema' => $schema,
),
)
);
}
add_action( 'init', 'unadorned_announcement_bar_settings' );
Loading the settings
You want to load the settings once you arrive at the settings page.
Whenever you want to trigger an action once the component is mounted, you can use the useEffect
hook. In this case, you want to make a request with apiFetch
to the Site Settings endpoint to load and use the saved settings.
Since all this is related to state management, modify your useSettings
custom hook located in the src/index.js
file accordingly:
import apiFetch from '@wordpress/api-fetch';
import { useEffect } from '@wordpress/element';
const useSettings = () => {
const [ message, setMessage ] = useState();
const [ display, setDisplay ] = useState();
const [ size, setSize ] = useState();
useEffect( () => {
apiFetch( { path: '/wp/v2/settings' } ).then( ( settings ) => {
setMessage( settings.unadorned_announcement_bar.message );
setDisplay( settings.unadorned_announcement_bar.display );
setSize( settings.unadorned_announcement_bar.size );
} );
}, [] );
// ...
};
The defaults defined in register_setting
are made available by the endpoint. For this reason, you no longer need to have default values in your useState
calls. Once the data is returned by the endpoint, you update the state with the setter functions. This process happens almost instantaneously.
Saving the settings
Just like how you’ve handled the data loading, you can use the Site Settings endpoint to persist the settings.
This time, the REST API call should not occur on page load, but when you click the SaveButton. Therefore, first encapsulate the apiFetch
in a function inside the useSettings
hook:
const useSettings = () => {
// ...
const saveSettings = () => {
apiFetch( {
path: '/wp/v2/settings',
method: 'POST',
data: {
unadorned_announcement_bar: {
message,
display,
size,
},
},
} );
};
return {
// ...
saveSettings,
};
};
Then, you can use exported saveSettings
function as the handler of the onClick
in your SaveButton
component:
const SettingsPage = () => {
const {
// ...
saveSettings,
} = useSettings();
return (
<>
<SettingsTitle/>
<Panel></Panel>
<SaveButton onClick={ saveSettings } />
</>
);
};
Now, by pressing the Save button, you can persist your changes. If you refresh your settings page, the saved settings will be loaded.
There are only a few more things missing, one of which is the front-end functionality.
Displaying the announcement bar on the front end
The most straightforward way to display the announcement bar is to output it right after the body
tag.
You can use the wp_body_open
hook, which nowadays is expected to be present in any theme. Add the following code to your index.php
file:
function unadorned_announcement_bar_front_page() {
$options = get_option( 'unadorned_announcement_bar' );
if ( ! $options['display'] ) {
return;
}
printf(
'<div>%s</div>',
esc_html( $options['message'] )
);
}
add_action( 'wp_body_open', 'unadorned_announcement_bar_front_page' );
Since there’s minimal styling required for the announcement bar, you can simply inline the CSS. Update the function you just created accordingly:
function unadorned_announcement_bar_front_page() {
// ...
printf(
'<div style="%s">%s</div>',
sprintf(
'background: var(--wp--preset--color--vivid-purple, #9b51e0); color: var(--wp--preset--color--white, #ffffff); padding: var(--wp--preset--spacing--20, 1.5rem); font-size: %s;',
esc_attr( $options['size'] )
),
esc_html( $options['message'] )
);
}
Or, if you want to avoid a lengthy inline CSS, you can use the compile_css
method of WP_Style_Engine
:
function unadorned_announcement_bar_front_page() {
// ...
$css = WP_Style_Engine::compile_css(
array(
'background' => 'var(--wp--preset--color--vivid-purple, #9b51e0)',
'color' => 'var(--wp--preset--color--white, #ffffff)',
'padding' => 'var(--wp--preset--spacing--20, 1.5rem)',
'font-size' => $options['size'],
),
''
);
printf(
'<div style="%s">%s</div>',
esc_attr( $css ),
esc_html( $options['message'] )
);
}
The result are the same:
The final touches
At this point, you can display the announcement bar and change its settings. Your plugin is functional, but with adding a few more details, you could make it more polished.
Adding notifications
When you save settings, there’s no visual indication that the process was successful. To improve this situation, adding a notification system would eliminate any guesswork.
It goes beyond the goals of this introductory article to explain some details of the notification’s implementation. But you’ll find a course mentioned at the end of the article that explains it in great detail. You would gain a lot by taking it.
Essentially, it comes down to two things. First, you have to trigger some type of notice.
You can make the following changes to your custom hook in the src/index.js
file:
import { useDispatch } from '@wordpress/data';
import { store as noticesStore } from '@wordpress/notices';
const useSettings = () => {
// ...
const { createSuccessNotice } = useDispatch( noticesStore );
// ...
const saveSettings = () => {
apiFetch( {
// ...
} ).then( () => {
createSuccessNotice(
__( 'Settings saved.', 'unadorned-announcement-bar' )
);
} );
};
// ...
};
For the next step, WordPress provides some components to abstract away most of the complexities.
With just a few lines, you can create a component that renders the notices, if there are any. You can use the following code and add it to the src/index.js
file:
import { useDispatch, useSelect } from '@wordpress/data';
import { store as noticesStore } from '@wordpress/notices';
import { NoticeList } from '@wordpress/components';
const Notices = () => {
const { removeNotice } = useDispatch( noticesStore );
const notices = useSelect( ( select ) =>
select( noticesStore ).getNotices()
);
if ( notices.length === 0 ) {
return null;
}
return <NoticeList notices={ notices } onRemove={ removeNotice } />;
};
As the last step, include the Notices
component in the existing SettingsPage
. Based on the mockup, you should add it after the SettingsTitle
:
const SettingsPage = () => {
// ...
return (
<>
<SettingsTitle/>
<Notices />
<Panel></Panel>
<SaveButton />
</>
);
};
Now, you should see a notification displayed whenever you save your settings.
Adding custom styles
The current settings page looks alright without any custom CSS. However, some minor adjustments are needed to match the design closely.
First, you must import the index.scss
to index.js
to compile the Sass file to a regular CSS:
import './index.scss';
// ..
domReady( () => {
// ...
} );
Then, you have to enqueue the CSS file. Add wp_enqueue_style
to the existing unadorned_announcement_bar_settings_page_enqueue_style_script
function located in the index.php
file:
function unadorned_announcement_bar_settings_page_enqueue_style_script( $admin_page ) {
// ...
wp_enqueue_style(
'unadorned-announcement-bar-style',
plugins_url( 'build/index.css', __FILE__ ),
array_filter(
$asset['dependencies'],
function ( $style ) {
return wp_style_is( $style, 'registered' );
}
),
$asset['version'],
);
}
You can safely remove the wp_enqueue_style( 'wp-components' )
, as it’s already included in the dependencies list.
All you need to do is tweak some of the spacings and widths. Add the following styles to the src/index.scss
file:
#unadorned-announcement-bar-settings {
max-width: 800px;
h1 {
padding-block: 8px;
}
.components-base-control:has(textarea) {
flex-basis: 400px;
}
.components-notice {
&, &__content {
margin: 0;
}
}
.components-notice-list {
display: flex;
flex-direction: column;
gap: 8px;
margin-block-end: 16px;
}
.components-panel {
margin-block-end: 8px;
}
}
With the styles added, your settings page should look more arranged:
Next steps
The repository and the playground
You can check the repository on GitHub to see the entire code together.
If you followed along and created the plugin step-by-step, you know it’s entirely possible to have all JavaScript code in the src/index.js
. In the repository, the JavaScript is split into multiple files to showcase how you could organize a more complex React application. However, essentially, it’s the same code.
If you want to interact with the plugin and see it in action without downloading and installing it, go to the prepared WordPress Playground.
Ideas for improvements
While the plugin is feature-complete, there’s always room for improvement.
So far, you’ve implemented successful notifications, but what about handling errors from the REST API?
The code could also account for cases where the wrong data type is returned since the value of the unadorned_announcement_bar
option in the options table could be updated by other means than the REST API.
Or you can explore adding a few more settings. For example, a control can be added to change the colors of the announcement bar. You could use the ColorPalette
component for that.
Diving deeper
To learn more about creating dynamic user interfaces with WordPress React components, you should continue with the Using the WordPress Data Layer course.
It provides a step-by-step and comprehensive introduction to the data layer and guides you through creating a more complex React app to manage WordPress pages. You can learn about other components and have an in-depth look at notifications.
Props to @greenshady, @bph for the editorial review, and to @carmen222, @strangerkir especially for their moral support.
Leave a Reply