WordPress Theme Development: Creating Custom Fields and Meta Boxes

WordPress Theme Development: Creating Custom Fields and Meta Boxes

/ 64 responses

In the first part of my WordPress tutorial series, we learned what WordPress is and how to create and use a basic theme. In part two, we learned more advanced concepts like adding comments and images.

In part three, we’re going to learn how to add basic custom fields to a post, save them in the database, and show them on the front end of the website, without a plugin. And it’s going to be easy, you’ll love it.



  • Create a custom post type.
  • Add a custom meta box.
  • Create the following custom fields:
    • Input text field
    • Textarea
    • Checkbox
    • Select Menu
    • Image (and image upload)

This tutorial is independent of the previous installments. If you’ve been following along, you can just add this on to what you already have. If not, you can still do this tutorial from scratch as long as you have a basic knowledge of WordPress.

I’m still confused. What’s going on?

There are a lot of WordPress-specific words going around already, and it’s more confusing than I’d like. The gist of it is, have you ever wanted to add an extra field to a WordPress post? Maybe a date, or a URL, or an e-mail address? We’re going to learn how to do that. Have you ever wanted to add another upload field for images outside of the “Featured Image” thumbnail option? In this article, we’ll also learn how to add an upload button and browse through the media gallery to insert an image.

It’s basically a simplified version of what Advanced Custom Fields does, but without any plugins.

I promise I’ll make it as simple as possible.

Now we’ll begin.

Anything throughout this article prefixed with your_ is custom, and you can change the name.

Create a Custom Post

I’m starting off with a completely empty WordPress theme, just like in part one. I’m going to create a custom post called Your Post, with the id your_post. You can call it whatever you want, but it might be easiest to practice the first time around with the same names I used.

Here’s the code that will go into functions.php.


function create_post_your_post() {
	register_post_type( 'your_post',
			'labels'       => array(
				'name'       => __( 'Your Post' ),
			'public'       => true,
			'hierarchical' => true,
			'has_archive'  => true,
			'supports'     => array(
			'taxonomies'   => array(
	register_taxonomy_for_object_type( 'category', 'your_post' );
	register_taxonomy_for_object_type( 'post_tag', 'your_post' );
add_action( 'init', 'create_post_your_post' );

You can go more in-depth about all the options of creating a custom post here. I made a very simple one. The most important things that it does:

  • Registers a post type called Your Post, with the id your_post.
  • Supports a titlethe_title(), editorthe_content(), excerptthe_excerpt(), and thumbnail/featured image – the_post_thumbnail(). (Must add theme support for the thumbnail).
  • Supports taxonomies, or ways to group posts, with tags and categories.

Displaying the Custom Post

Now, to display the post on the front end. You can display the contents of a custom post anywhere. I’m just going to put it in page.php for testing purposes, so any page I make will display my custom loop.


$args = array(
	'post_type' => 'your_post',
$your_loop = new WP_Query( $args ); 

if ( $your_loop->have_posts() ) : while ( $your_loop->have_posts() ) : $your_loop->the_post(); 
$meta = get_post_meta( $post->ID, 'your_fields', true ); ?>

<!-- contents of Your Post -->

<?php endwhile; endif; wp_reset_postdata(); ?>

The variable $your_loop can be anything, I’m just sticking to a common theme of your_ throughout this article so you know what you can change. $meta = get_post_meta( $post->ID, 'your_fields', true ); is not necessary right now, but will be essential later.

Back End

Updating the title and editor fields like in a regular post…

Screen Shot 2016-08-09 at 3.16.33 PM


Inserting the template codes like normal…

<?php the_title(); ?>

<?php the_content(); ?>
Front End

And here we are. I have no styles or anything, because it’s not necessary for the point of the article, and I don’t like adding unnecessary complexity.

Screen Shot 2016-08-09 at 3.22.47 PM

Alright, so the custom post is all set up now.

Create a Meta Box

From here, the original source for much of the code comes from WordPress Meta Boxes: a Comprehensive Developer’s Guide by Alex Mansfield, and Create WordPress Post Custom Meta Boxes by Paulund. Both of those are very good, detailed articles, and I’ve attempted to simplify some of those processes here.

Here’s the code to add a meta box.

function add_your_fields_meta_box() {
		'your_fields_meta_box', // $id
		'Your Fields', // $title
		'show_your_fields_meta_box', // $callback
		'your_post', // $screen
		'normal', // $context
		'high' // $priority
add_action( 'add_meta_boxes', 'add_your_fields_meta_box' );

More detail on this function can be found here. The actual function here is add_meta_box( $id, $title, $callback, $screen, $context, $priority ). The most important ones are $title, which is Your Fields, and $screen (or page), which is where the meta box will be added. You can add it to a regular post or page, among other things, but I chose to add it to your_post, our custom post from earlier.

Now if you go back into your post, you’ll see this below the editor.

Screen Shot 2016-08-09 at 3.54.09 PM

It’s an empty meta box! Additionally, if you click on Screen Options at the top of the post, you’ll see Your Fields in the options.

Screen Shot 2016-08-09 at 3.56.09 PM

Neat. Now it’s time to start putting stuff in there.

Save Fields in the Database

First, the function that will display all your custom fields.

function show_your_fields_meta_box() {
	global $post;  
		$meta = get_post_meta( $post->ID, 'your_fields', true ); ?>

	<input type="hidden" name="your_meta_box_nonce" value="<?php echo wp_create_nonce( basename(__FILE__) ); ?>">

    <!-- All fields will go here -->

	<?php }

We’ll return to <!-- All fields will go here --> in a moment. For now, directly below the above function, we’re going to paste in this big chunk of code (modified from Paulund) that will save all your_fields to the database.

function save_your_fields_meta( $post_id ) {   
	// verify nonce
	if ( !wp_verify_nonce( $_POST['your_meta_box_nonce'], basename(__FILE__) ) ) {
		return $post_id; 
	// check autosave
	if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
		return $post_id;
	// check permissions
	if ( 'page' === $_POST['post_type'] ) {
		if ( !current_user_can( 'edit_page', $post_id ) ) {
			return $post_id;
		} elseif ( !current_user_can( 'edit_post', $post_id ) ) {
			return $post_id;
	$old = get_post_meta( $post_id, 'your_fields', true );
	$new = $_POST['your_fields'];

	if ( $new && $new !== $old ) {
		update_post_meta( $post_id, 'your_fields', $new );
	} elseif ( '' === $new && $old ) {
		delete_post_meta( $post_id, 'your_fields', $old );
add_action( 'save_post', 'save_your_fields_meta' );

Make sure your_meta_box_nonce matches the name attribute, and you’ve specified your_fields in the meta box function. This code is verifying the Nonce from the first function, making sure the user has the correct permissions to update the fields, and updating the post meta fields.

It’s a confusing block of code at first, but fortunately you don’t have to do much with this one besides make sure the id of the post matches the custom meta box you created.

Create Custom Fields

Now we’re going to return to <!-- All fields will go here -->. This is where we’re going to make our input text field, textbox, checkbox, select menu, and image upload. Right above that, we created a variable called $meta. This reaches into the your_fields table in the database and retrieves the information: $meta = get_post_meta( $post->ID, 'your_fields', true );. We’re going to make an array and put all of our custom fields in it.

You can use custom CSS to make your admin panel look nice, which I can possibly go into further if requested, but getting the basic functionality is more important to me for now, so I’m just going to use the default styles. Wrapping everything in a <p> isn’t semantically correct, but it’s keeping it organized for now.

Text Input

I’m going to add a regular text input. The regular-text class is just a built in WordPress admin style. Whatever you put in the straight brackets will be the code for your custom field. If I wanted to make this text field an e-mail address, for example on a custom post called “Team Members” with contact information for each team member, I might call it your_fields[email] instead of your_fields[text].

	<label for="your_fields[text]">Input Text</label>
	<input type="text" name="your_fields[text]" id="your_fields[text]" class="regular-text" value="<?php echo $meta['text']; ?>">

The code for the text area is almost the same as the input, except the value is echoed out between the tags instead of as an attribute. The rows, cols, and style placed on it doesn’t really matter. It’s important to leave no space between the tags, to ensure no extra space ends up in your textbox.

	<label for="your_fields[textarea]">Textarea</label>
	<textarea name="your_fields[textarea]" id="your_fields[textarea]" rows="5" cols="30" style="width:500px;"><?php echo $meta['textarea']; ?></textarea>

There might be several ways to implement the checkbox, but this is one way that worked for me.

	<label for="your_fields[checkbox]">Checkbox
		<input type="checkbox" name="your_fields[checkbox]" value="checkbox" <?php if ( $meta['checkbox'] === 'checkbox' ) echo 'checked'; ?>>
Select Menu

You can include as many options as you want here, but I’m just doing two for the example.

	<label for="your_fields[select]">Select Menu</label>
	<select name="your_fields[select]" id="your_fields[select]">
			<option value="option-one" <?php selected( $meta['select'], 'option-one' ); ?>>Option One</option>
			<option value="option-two" <?php selected( $meta['select'], 'option-two' ); ?>>Option Two</option>

The image upload is going to be the most complicated one. The actual display code isn’t any more complicated than the other fields have been. It’s important to include the meta-image class on the text input, and image-upload class on the submit button. I just put some quick max-width styling on the image for a simple preview.

	<label for="your_fields[image]">Image Upload</label><br>
	<input type="text" name="your_fields[image]" id="your_fields[image]" class="meta-image regular-text" value="<?php echo $meta['image']; ?>">
	<input type="button" class="button image-upload" value="Browse">
<div class="image-preview"><img src="<?php echo $meta['image']; ?>" style="max-width: 250px;"></div>

Actually getting this to do anything is the more complicated part. Directly below, you can place this JavaScript snippet (modified from Theme Foundation).

This code will open the built in WordPress media gallery when you click browse, and insert the selected URL into the input field.

jQuery(document).ready(function ($) {

	// Instantiates the variable that holds the media library frame.
	var meta_image_frame;
	// Runs when the image button is clicked.
	$('.image-upload').click(function (e) {
		var meta_image = $(this).parent().children('.meta-image');

		// If the frame already exists, re-open it.
		if (meta_image_frame) {
		// Sets up the media library frame
		meta_image_frame = wp.media.frames.meta_image_frame = wp.media({
			title: meta_image.title,
			button: {
				text: meta_image.button
		// Runs when an image is selected.
		meta_image_frame.on('select', function () {
			// Grabs the attachment selection and creates a JSON representation of the model.
			var media_attachment = meta_image_frame.state().get('selection').first().toJSON();
			// Sends the attachment URL to our custom image input field.
		// Opens the media library frame.

And that’s everything that needs to go into the functions.php file! As with the CSS styling from earlier, we can place this JavaScript in a separate file and call it on the admin pages, but for simplicity I’m just including it in the functions.php file for now. At the end of the article, I’ll include the entire code in case you got lost anywhere along the way.

Display the Output

Now, we have all our fields, and they all appear in the Your Fields meta box. I’m going to fill them all out, check the checkbox, select an option, upload an image of the majestic Doge Lion, and publish the post.

These will all go in the $your_loop query. Make sure $meta = get_post_meta( $post->ID, 'your_fields', true ); is in your loop.

Screen Shot 2016-08-10 at 12.51.25 PM

Text Input

Simple output of the text field.

<h1>Text Input</h1>
<?php echo $meta['text']; ?>

Simple output of the textarea.

<?php echo $meta['textarea']; ?>

I’m going to check if the checkbox has been checked or not, and display a message based on it.

<?php if ( $meta['checkbox'] === 'checkbox') { ?>
Checkbox is checked.
<?php } else { ?> 
Checkbox is not checked. 
<?php } ?>

I’ll show you two things you can do with the select. The basic output will be the value attribute.

<h1>Select Menu</h1>
<p>The actual value selected.</p>
<?php echo $meta['select']; ?>

You can also use a PHP switch statement to display a specific message based on the value selected.

<p>Switch statement for options.</p>
	switch ( $meta['select'] ) {
		case 'option-one':
			echo 'Option One';
		case 'option-two':
			echo 'Option Two';
			echo 'No option selected';

And finally, displaying the image.

<img src="<?php echo $meta['image']; ?>">

Here is the final output of everything I filled out earlier.

Screen Shot 2016-08-10 at 1.14.30 PM


If you had any trouble following along the way, here are the final files for page.php and functions.php on GitHub Gist.

Functions Page

This article only goes over the most basic way to add custom fields and update the database. There are a thousand more ways to do it, and plugins like ACF that will do it for you as well. I think it’s useful to know how to interact with the database and learn how to do it without a plugin.

It’s also a good idea to sanitize the output of all your fields.

If you’ve found any errors, or have any additional information that would make this better or more simple to understand, please let me know!

Thank you for reading! I'm Tania Rascia, and I write no-nonsense guides for designers and developers. If my content has been valuable to you, please help me keep the site ad-free and continuously updated with quality tutorials and material by making a donation. Any amount helps and is greatly appreciated! Otherwise, let me know any ideas you have on a course you'd be eager to see.

Write a response

Your email address will not be published.


  • thapachaki says:

    Maybe you can use this code for skip the warnings:

    Input Text

    <input type="text" name="own_fields[text]" id="own_fields[text]" class="regular-text" value="”>


    <input type="checkbox" name="own_fields[checkbox]" value="checkbox" >

    Select Menu

    <option value="option-one" >Option One
    <option value="option-two" >Option Two

    Image Upload
    <input type="text" name="own_fields[image]" id="own_fields[image]" class="meta-image regular-text" value="”>

    <img src="” style=”max-width: 250px;”>

  • jonny says:

    hey there, changing $meta = get_post_meta( $post->ID, ‘your_fields’, true ); ?>
    to $meta = [get_post_meta( $post->ID, ‘your_fields’, true )]; ?> fixed the string-off set error for me.

  • larp says:

    i have this problem in creating custom field. there is a warning in the fields,
    Warning: Illegal string offset ‘text’ in D:\xampp\htdocs\test_wordpress\wordpress\wp-content\themes\startwordpress\functions.php on line 145“. and it is not displaying the ouput in the page.php. please help me. 🙂

  • Dona says:

    How do you apply the same idea on category? Like adding an image and extra field on category?

  • Pisuke says:

    Is it possible to keep the html code separated? any tips on how to do this?

    I’ve tried to use get_template_part for the metabox html code, but it doesn’t work.

  • Jaime Enrique says:

    First I want to thank You Tania for doing this tutorial, it was very helpful for me to understand the way wordpress handle templates.

    I only have one question, i’ve tried using plugins with this theme but they don’t seem to work properly, for example im using a page composer and i’ve tried a lot of slides and none seems to work, i wonder if maybe u know why.

    thanks a lot

    best regards

  • Sam Cromwell says:

    Hello. I’m adding custom fields to posts & pages using your tutorial.
    It works nicely if i need to add only one custom field (but in the function to show meta box i need to pass parameters without offset – your_fields not your_fields[text] and $meta not $meta[‘text’] -.

    The troubles come when there are many fields there: besides the illegal offset issue (solved setting ‘false’ in get_post_meta), my custom fields won’t save.
    Did you find a solution for that? Thank you.

  • SONB says:

    Thank you for this great tutorial!

    I have a tiny issue with the image upload here. The text field is instantly populated with the url of the image but the preview is only displayed if I publish or update my post. I checked your and my code but couldn’t find any inconsistencies. The source code shows no url in .

    Not a big deal for me but an instant preview would be great.
    Thanks for any help!

    • Tania says:

      I had the same issue. I tried for a long time but I couldn’t figure it out. If you figure anything out let me know and I’ll improve the article!

      • SONB says:

        I found a solution.
        It seems like the custom fields on backend are updated by JS, and on frontend by $meta[‘…’]. The JS in your code is taking care only of the text field but not the . I made a small addition to your JS inside the $(‘.image-upload’).click() function and it works perfectly for the image preview:

        var meta_image_preview = $(this).parent().parent().children(‘.image-preview’);

        meta_image_frame.on(‘select’, function() {

        meta_image_preview.children(‘img’).attr(‘src’, media_attachment.url);

        Please feel free to optimize my changes, cause I’m not a pro in JS/jQuery.
        Thanks again for your great tutorials!

      • Tania says:

        Awesome! I’ll test it out then update the article. I spent forever trying to figure out the correct JS to get that to work.

  • Krisztina Kun says:

    Thanks for this! I tried a number of other tutorials on meta boxes, and yours made the most sense – I think I can actually recreate this in the future 🙂

  • Irfan Shaikh , Dewas , India says:

    Could you possibly write something on firebug and its use in Mozilla?
    Please it would be a great help to us!

  • Irfan Shaikh , Dewas , India says:

    Nice Article indeed , it would me too long to understand but finally I got it.
    Great work!

  • Chirag says:

    Thank you for this wonderful tutorial. I am a beginner level developer, and I was able to follow your tutorial and write my own theme using bootstrap css. However, my problem is that when I apply my theme to my website the content does not appear the way it shows it in the WYSIWYG Visual Editor (it works fine for other themes). Is there some additional change I need to make to the CSS or anywhere else so that the themes are WYSIWYG compatible?

  • Marisabel says:

    Hi! I love your tutorial. Do you any book about wordpress for creating plugins or advanced themes (as ecommerce)?

  • Eddy says:

    On my way to start building my own customizable theme. A great tutorial to start learning wordpress infrastructure & way of usage. Could not actually leave without leaving a comment 🙂

  • Anonymous says:

    It’s the best WordPress tutorial series i have ever read till now.

  • Rekijan says:

    I can save a post and have it show up but the data does not appear in the your fields input fields. For example if in the input field I type “input text content” it displays it on the page.php but when I want to edit it the input field is empty and if I publish again the text (“input text content”) is gone as well.

    Not sure if its related but I got an Illegal string offset warning which I fixed by changing $meta to an array:

    $meta = array(get_post_meta( $post->ID, 'your_fields', true )); ?>

  • Jonathan Farrell says:

    Hi, thank you so much for taking the time to construct this tutorial, it’s fantastic.

    I’ve got one question. With bootstrap the menu is dynamically changing depending on screen size. The only thing you don’t cover is how to include the custom menus from the theme customizer and how to keep the dynamic nature. I’m hoping this can be easily explained as it’s the only sticking point I have with my new site.

    • Tania says:

      Ah, I guess I haven’t gone into menus. You would make a menu by putting this in functions.php:

      function register_my_menu() {
        register_nav_menu('nav-menu',__( 'Nav Menu' ));
      add_action( 'init', 'register_my_menu' );

      And include this in your header.php:

      wp_nav_menu( array( 'theme_location' => 'nav-menu' ) );
      • Varun says:

        Hello Tania,
        Registered my menu in functions.php
        Menu is displaying in the navbar, but sub menu links are missing.
        How can we solve this?

      • max.shohel says:

        In this case, you can use the bootstrap navbar walker menu.

        ‘theme_location’ => ‘primary’,
        ‘depth’ => 2,
        ‘container’ => ‘div’,
        ‘container_class’ => ‘collapse navbar-collapse’,
        ‘container_id’ => ‘bs-example-navbar-collapse-1’,
        ‘menu_class’ => ‘nav navbar-nav’,
        ‘fallback_cb’ => ‘wp_bootstrap_navwalker::fallback’,
        ‘walker’ => new wp_bootstrap_navwalker())

      • Simon says:

        where in the header.php exactly

      • sayalok says:

        are you a teacher ? or u wanted to be a teacher ?

      • shriram says:

        where we have to add this code
        wp_nav_menu( array( ‘theme_location’ => ‘nav-menu’ ) );

        in header.php please help

  • Amandeep Singh says:

    Awesome tutorial

  • Marcus says:

    Great tutorial!

    I have a question:
    Actually I have blog posts section and other post section called “catalog”. They are completely different, catalog has 14 fields also title and featured image. How can I set different files to show them? Actually page-catalog.php list all catalog items and content-single.php shows specific item from catalog.

    I need other file to show blog posts not content-single.php. Any suggestion?

  • Hafizh says:

    I feel ready to write my own theme. yeah

  • William says:

    Thanks, Tania.
    I learned a lot from your articles.

  • Mikulas Stec says:

    Hi Tania,
    I like your easy-to-understand language. One may feel the “Simplicity-first approach” and I really love that. Thanks.

  • navin says:

    really , its great article for me, which help me more during my theme development.

    Thank you

  • V Ram says:

    Very nice and very useful
    thank you

  • Jitu says:

    Hey! ur tutorial was really good!…some of the best material i have ever found on the web ….keep it up!!! and thanks for sharing

  • Hey, thanks for this great article though I haven’t read it but copied it for later reading (hope you didn’t mind).

  • Pierre says:

    Hi Tania,
    I achieved my goal of trying to understand, as a newbie, how WordPress themes work. Thank you for your excellent, easy to understand articles.

  • Mel Macaluso says:

    Hi Tania,

    Beautiful tutorial, very well explained and easy to follow for beginners. Good job!

  • keerthy says:

    You’re work is wonderful well explained, thank you very much for all your time & efforts to made it in complete shape. Once again thank you very much.

  • Shanon Walter says:

    can you tell me how to have search box in this wordpress theme?

  • pratik ranpara says:

    Just brilliant, was in hurry to complete a project, and your article just made my day!
    thanks alot for this post.

  • Ivan says:

    Thanks for sharing your knowledge.
    I’ve followed the tutorial and I couldn’t display the custom post page.
    I do display another custom post type (taken from Part 1 of this series, renamed to ‘book-review’) though it took a long time between publishing and actual display.
    But the Part 3 of your great tutorial, I couldn’t see the results.
    And yes, I’ve just copied the code from the files above.
    Using WP 4.6.
    I include the URL in the Website field.

    • Nate says:

      I had a similar problem and found a fix, at least for my setup.

      Initially, I used the theme files built from the previous tutorial (instead of a blank theme, like Tania said to do… oops!)

      If you’re doing the same, the custom post type (your_post) may not display using page.php due to a conflict with the WordPress template hierarchy.

      In my case, it seems like the index.php template was taking precedent over page.php. For the sake of simplicity, I simply swapped the loop in my index.php file for the custom loop created in this tutorial. It’s not a good idea to do this on a theme meant for production, but it let me finish tutorial and that’s all I cared about.

      Alternatively, the problem may be your functions.php file. I had to triple check for typos because I didn’t just copy and paste everything directly. I’d copy and past Tania’s example code exactly just to be sure.

  • Robert Keli says:

    Hello over there,
    Nice tutorial.
    The series of tutorials have helped me write a completely custom theme,
    However I have a problem here.
    Only the first post displays the Date it was published.
    The preceeding older posts don’t display their dates,
    How do I solve this?Could it be a looping problem yet the posts show the number of comments and the article writer?
    Thanks alot

  • Lon Hosford says:

    Articles 1 and 2 went great.
    Kinda stuck on this one.
    1. You mean by ‘I’m starting off with a completely empty WordPress theme, just like in part one.” what was accomplished at the end of the first part and not at the start correct?
    2. After getting it all together, I get showing in the field input areas for the admin area:
    PHP Warning: Illegal string offset 'image' in ...functions.php on line 72
    PHP Warning: Illegal string offset 'image' in ...functions.php on line 69
    PHP Warning: Illegal string offset 'select' in ...functions.php on line 64
    PHP Warning: Illegal string offset 'select' in ...functions.php on line 63
    PHP Warning: Illegal string offset 'checkbox' in ...functions.php on line 56
    PHP Warning: Illegal string offset 'textarea' in ...functions.php on line 52
    PHP Warning: Illegal string offset 'text' in ...functions.php on line 47

    Using WP 4.6

  • ABM says:

    Hello Tania,
    I am newbie in designing a website . I want you to guide me and give an instruction to start. I learnt HTML and CSS in http://www.codecademy.com but I think I am not in a nice level. Could you please give me the instruction to start ?

  • Luis Lopez says:

    Very well explained, as I see, the way of doing the WP loop and the PHP syntax has changed a lot, last time I did PHP and or WordPress was in 2008 and now I’m more into the NodeJS side of things. I’m looking forward to following this tutorial from scratch and I want to congratulate you for the effort, very well written and I hope it reaches as many people as it needs to.

Tania's List

My tutorials, guides, and articles for web designers, developers, and autodidacts, sent out once a month or so. No bullshit, ads, or tricks.