2026年11月12日
2026年11月12日
How to Build a Custom WordPress Meta Box (add_meta_box and Saving)
This article is a localized edition of the Japanese original. Read the Japanese original
A meta box is a group of input fields shown on the post or page edit screen. With WordPress's add_meta_box() you can add your own meta box and save the entered values as post metadata (custom fields).
This article builds an example that adds four fields to book review posts: "book price", "ISBN", "featured" and "internal notes", going from registration to saving to displaying the values in a theme. Put the code in your child theme's functions.php or in your own plugin. The functions and arguments were checked against the function reference on developer.wordpress.org as of November 2026.
The overall flow
| Stage | Hook / function | What happens |
|---|---|---|
| Register | add_meta_boxes hook, add_meta_box() |
Shows the meta box on the edit screen |
| Render | Callback function, wp_nonce_field() |
Outputs the fields and a nonce |
| Save | save_post hook, update_post_meta() |
Validates, then saves to post meta |
| Display | get_post_meta() |
Outputs the values in a theme template |
Step 1: Register the meta box
<?php
add_action( 'add_meta_boxes', 'myprefix_add_book_meta_box' );
function myprefix_add_book_meta_box() {
add_meta_box(
'myprefix_book_details', // Meta box ID (also used as the HTML id attribute)
'Book details', // Heading
'myprefix_render_book_meta_box', // Function that outputs the content
'post', // Post type to show it on (an array is also allowed)
'normal', // Position: normal / side / advanced
'high' // Priority: high / core / default / low
);
}
The arguments of add_meta_box() are, in order: ID, heading, callback, screen (post type), position, priority, and extra values passed to the callback. Change 'post' to array( 'post', 'page' ) to show the box on both posts and pages. For a custom post type, pass that post type's name.
If you pass an array as the seventh argument, the callback receives it in $box['args'] through its second argument $box.
Step 2: Render the fields
<?php
function myprefix_render_book_meta_box( $post, $box ) {
// Output the nonce used to verify the save request
wp_nonce_field( 'myprefix_save_book', 'myprefix_book_nonce' );
$price = get_post_meta( $post->ID, '_myprefix_book_price', true );
$isbn = get_post_meta( $post->ID, '_myprefix_book_isbn', true );
$featured = get_post_meta( $post->ID, '_myprefix_book_featured', true );
$notes = get_post_meta( $post->ID, '_myprefix_book_notes', true );
?>
<p>
<label for="myprefix_book_price">Price (JPY)</label><br>
<input type="number" id="myprefix_book_price" name="myprefix_book_price"
value="<?php echo esc_attr( $price ); ?>" min="0" step="1">
</p>
<p>
<label for="myprefix_book_isbn">ISBN</label><br>
<input type="text" id="myprefix_book_isbn" name="myprefix_book_isbn"
value="<?php echo esc_attr( $isbn ); ?>" class="regular-text">
</p>
<p>
<label>
<input type="checkbox" name="myprefix_book_featured" value="1"
<?php checked( $featured, '1' ); ?>>
Show as featured
</label>
</p>
<p>
<label for="myprefix_book_notes">Internal notes (not shown on the site)</label><br>
<textarea id="myprefix_book_notes" name="myprefix_book_notes" rows="3"
class="large-text"><?php echo esc_textarea( $notes ); ?></textarea>
</p>
<?php
}
Key points:
- When you output values into fields, escape them:
esc_attr()for attribute values andesc_textarea()for a textarea. checked()outputschecked="checked"when its two values are equal.regular-textandlarge-textare standard admin classes, so field widths line up without any custom CSS.
Step 3: Save the values
Use the save_post hook to validate the submitted values before saving them.
<?php
add_action( 'save_post_post', 'myprefix_save_book_meta' );
function myprefix_save_book_meta( $post_id ) {
// Do nothing if the nonce is missing or invalid
if ( ! isset( $_POST['myprefix_book_nonce'] )
|| ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['myprefix_book_nonce'] ) ), 'myprefix_save_book' ) ) {
return;
}
// Don't save on autosave or for revisions
if ( ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) || wp_is_post_revision( $post_id ) ) {
return;
}
// Does the user have permission to edit this post?
if ( ! current_user_can( 'edit_post', $post_id ) ) {
return;
}
// Price: integer of 0 or more. Delete it if left empty
if ( isset( $_POST['myprefix_book_price'] ) && '' !== $_POST['myprefix_book_price'] ) {
update_post_meta( $post_id, '_myprefix_book_price', absint( $_POST['myprefix_book_price'] ) );
} else {
delete_post_meta( $post_id, '_myprefix_book_price' );
}
// ISBN: single-line text
if ( isset( $_POST['myprefix_book_isbn'] ) ) {
update_post_meta( $post_id, '_myprefix_book_isbn', sanitize_text_field( wp_unslash( $_POST['myprefix_book_isbn'] ) ) );
}
// Checkbox: an unchecked box is not submitted, so save '0' in that case
update_post_meta( $post_id, '_myprefix_book_featured', isset( $_POST['myprefix_book_featured'] ) ? '1' : '0' );
// Internal notes: text that keeps line breaks
if ( isset( $_POST['myprefix_book_notes'] ) ) {
update_post_meta( $post_id, '_myprefix_book_notes', sanitize_textarea_field( wp_unslash( $_POST['myprefix_book_notes'] ) ) );
}
}
save_post_postis a hook of the formsave_post_{post_type}and runs only when a post of typepostis saved. If you usesave_post, which runs for every post type, check the post type inside the function.- WordPress adds slashes to
$_POSTvalues, so remove them withwp_unslash()before sanitizing. - The nonce check protects against forged save requests sent from other sites (CSRF). Don't skip it, and don't skip the capability check either.
Step 4: Remove unneeded meta boxes and show boxes by role
Standard meta boxes can be removed with remove_meta_box(). Register the removal with a higher priority number so it runs after your own meta box is added.
<?php
add_action( 'add_meta_boxes', 'myprefix_remove_meta_boxes', 99 );
function myprefix_remove_meta_boxes() {
// Remove the Custom Fields box
remove_meta_box( 'postcustom', 'post', 'normal' );
// Hide the meta box with internal notes from non-administrators
if ( ! current_user_can( 'manage_options' ) ) {
remove_meta_box( 'myprefix_book_details', 'post', 'normal' );
}
}
The arguments of remove_meta_box() are ID, post type and position. Use the same position as when the box was registered. In the Classic Editor, trackbacksdiv (send trackbacks), commentstatusdiv (discussion), slugdiv (slug) and others can be removed the same way. In the block editor, settings panels such as "Discussion" are displayed by a different mechanism than meta boxes, so this method may not remove them.
Step 5: Display the values in a theme
Retrieve the saved values with get_post_meta() in a theme file such as single.php. This example goes inside the Loop.
<?php
$price = get_post_meta( get_the_ID(), '_myprefix_book_price', true );
$isbn = get_post_meta( get_the_ID(), '_myprefix_book_isbn', true );
$featured = get_post_meta( get_the_ID(), '_myprefix_book_featured', true );
if ( '' !== $price || '' !== $isbn ) : ?>
<div class="book-info">
<?php if ( '1' === $featured ) : ?>
<span class="book-info__badge">Featured</span>
<?php endif; ?>
<dl>
<?php if ( '' !== $price ) : ?>
<dt>Price</dt>
<dd><?php echo esc_html( number_format( (int) $price ) ); ?> JPY</dd>
<?php endif; ?>
<?php if ( '' !== $isbn ) : ?>
<dt>ISBN</dt>
<dd><?php echo esc_html( $isbn ); ?></dd>
<?php endif; ?>
</dl>
</div>
<?php endif;
With true as the third argument, get_post_meta() returns a single value as a string. If nothing is saved, it returns an empty string, so check for that before output. The internal notes are not meant for the site, so the template doesn't retrieve them.
Meta keys and the block editor
- Meta keys starting with an underscore (for example
_myprefix_book_price) are treated as "protected meta" and don't appear in the standard Custom Fields box. They suit values you want edited only through your meta box. - Give key names a prefix. Short names like
_pricemay collide with names used by plugins such as WooCommerce. - It works in the block editor too. Meta boxes added with
add_meta_box()appear below the content (or in the right sidebar) in the block editor and are saved together with the post. - If you want to read and write the values from the REST API or blocks, you can register the meta key with
register_post_meta()and enableshow_in_rest.
When to use ACF instead
If you have many fields, need repeating fields or image pickers, or want to manage fields without code, building them with a plugin such as Advanced Custom Fields (ACF) is quicker. If you only need a few fields, don't want another plugin, or want to ship the fields with a theme or your own plugin, building them with add_meta_box() as shown here is a good fit.
For the basics of custom fields, see the article on meta boxes and custom fields; for what to check when values aren't saved, see what to do when custom fields are not saved.
Summary
- Call
add_meta_box()on theadd_meta_boxeshook, and output the fields and a nonce in the callback. - Save on
save_post(orsave_post_{post_type}): check the nonce, autosave and capability, applywp_unslash()and sanitizing, then save withupdate_post_meta(). - Remove unneeded meta boxes by calling
remove_meta_box()insideadd_meta_boxeswith a higher priority number. - On the display side, retrieve values with
get_post_meta()and escape them on output.
Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)