WordPress Custom Taxonomies and Custom Fields — ACF and Meta Boxes Complete Guide
In this tutorial, you'll learn to create custom taxonomies and custom fields in WordPress using register_taxonomy(), Advanced Custom Fields, and manual meta box development with add_meta_box() and save_post.
What You'll Learn
- What custom taxonomies are and how they group and filter content beyond categories and tags
- How register_taxonomy() works with all its key parameters
- The difference between hierarchical and non-hierarchical taxonomies
- How to create category-style and tag-style taxonomies
- What custom fields store in wp_postmeta and how to retrieve them
- How Advanced Custom Fields (ACF) simplifies field creation
- How to create field groups with location rules in ACF
- How to display ACF fields in templates
- How to build manual meta boxes with add_meta_box and save_post
- How to use meta_query in WP_Query for filtering by custom field values
Why It Matters
Categories and tags work fine for a blog, but real-world sites need more structure. A real estate site needs to filter properties by "Location" (hierarchical: City > Neighborhood) and "Property Type" (house, apartment, condo). A recipe site needs "Cuisine" and "Diet" taxonomies plus custom fields for ingredients, prep time, and difficulty. Without custom taxonomies and fields, developers resort to hacking categories or storing data in post content — both lead to unmaintainable messes. Taxonomies organize. Fields store.
Real-World Use
A job board site uses a "Job Category" taxonomy (hierarchical: Technology > Software > Frontend) and a "Job Type" taxonomy (non-hierarchical: full-time, part-time, contract, remote). Each job listing has custom fields for salary range, application deadline, and company name. A Visitor filters jobs by category and job type simultaneously. The API serves filtered results to a React front end. Without custom taxonomies and fields, this site would require a custom database and significant custom code.
Learning Path
flowchart LR
A[Custom Post Types] --> B["Custom Taxonomies & Fields
You are here"]:::current
B --> C[WP_Query & The Loop]
C --> D[Hooks]
D --> E[Shortcodes]
classDef current fill:#38bdf8,color:#0f172a,stroke-width:2px
What Are Custom Taxonomies?
A taxonomy is a way to group things. WordPress ships with two built-in taxonomies: Category (hierarchical) and Tag (non-hierarchical). A custom taxonomy is one you register yourself.
Think of taxonomies as the labeling system in a warehouse. Categories are like warehouse sections with aisles and shelves inside them. Tags are like sticky notes you can attach to any box — they help you find things but have no hierarchy.
register_taxonomy() Function
Register a taxonomy using register_taxonomy() hooked to init, just like CPTs:
function dodatech_register_location_taxonomy() {
$labels = array(
'name' => 'Locations',
'singular_name' => 'Location',
'search_items' => 'Search Locations',
'all_items' => 'All Locations',
'parent_item' => 'Parent Location',
'parent_item_colon' => 'Parent Location:',
'edit_item' => 'Edit Location',
'update_item' => 'Update Location',
'add_new_item' => 'Add New Location',
'new_item_name' => 'New Location Name',
'menu_name' => 'Location',
);
register_taxonomy(
'location',
array( 'property' ),
array(
'labels' => $labels,
'hierarchical' => true,
'public' => true,
'show_in_rest' => true,
'rewrite' => array( 'slug' => 'location' ),
'show_admin_column' => true,
)
);
}
add_action( 'init', 'dodatech_register_location_taxonomy' );
Key Parameters
| Parameter | Type | Description |
|---|---|---|
hierarchical |
bool | true = category-like (parent/child), false = tag-like |
show_in_rest |
bool | Enable for Gutenberg and REST API support |
rewrite |
array/boolean | URL slug for taxonomy archive pages |
show_admin_column |
bool | Show taxonomy column in post list table |
show_in_menu |
bool | Show in WordPress admin menu |
show_in_quick_edit |
bool | Show in quick edit screen |
Hierarchical vs Non-Hierarchical
Hierarchical taxonomies behave like categories. They support parent-child relationships. You can have "Technology > Programming > PHP". The term archive URL might be /location/united-states/new-york/. Terms display as a checkbox hierarchy in the editor.
Non-hierarchical taxonomies behave like tags. Terms are flat with no parent-child structure. The archive URL is /tag/{term-slug}/. Terms display as a text input where you type and autocomplete.
Creating a Non-Hierarchical Taxonomy
function dodatech_register_feature_taxonomy() {
register_taxonomy(
'feature',
'property',
array(
'hierarchical' => false,
'labels' => array(
'name' => 'Features',
'singular_name' => 'Feature',
'search_items' => 'Search Features',
'popular_items' => 'Popular Features',
'add_new_item' => 'Add New Feature',
),
'show_in_rest' => true,
'rewrite' => array( 'slug' => 'feature' ),
)
);
}
add_action( 'init', 'dodatech_register_feature_taxonomy' );
Notice popular_items in the labels. Tag-style taxonomies use popular_items for the tag cloud. Category-style taxonomies use parent_item. The labels array should match the taxonomy type.
Custom Fields Overview
Custom fields store additional data about a post in the wp_postmeta database table. Each meta entry has four columns: meta_id, post_id, meta_key, and meta_value. This key-value storage is simple but powerful.
WordPress has a built-in custom fields meta box that you enable with 'supports' => array( 'custom-fields' ) in register_post_type(). It shows a key-value pair interface, but navigating it is awkward. That is why developers use plugins like Advanced Custom Fields or build their own meta boxes.
Advanced Custom Fields (ACF)
ACF is the most popular custom fields plugin, with over 2 million installations. It provides a visual interface for creating field groups and assigning them to post types, taxonomies, pages, and more.
Creating a Field Group
After installing ACF, go to Custom Fields > Add New. You define:
- Field Group Title — internal name like "Property Details"
- Fields — individual fields with type, label, name, and settings
- Location Rules — where this group appears
Location Rules
Location rules are the most powerful feature of ACF. A field group can appear based on:
- Post type equals "property"
- Taxonomy term equals "location"
- Page template equals "full-width.php"
- Post status equals "published"
- User role equals "editor"
Multiple rules create complex conditions. You can say: show this group when "post type is property" OR "post type is agent AND taxonomy is location".
Common Field Types
| Field Type | Use Case | PHP Return Value |
|---|---|---|
| Text | Title, short text | string |
| WYSIWYG | Rich content | HTML string |
| Image | Single image | array (id, url, alt) or ID |
| Repeater | Repeatable rows (features list) | array of sub-field arrays |
| Flexible Content | Layout blocks | array of layout arrays |
| Gallery | Multiple images | array of image arrays |
| Select | Dropdown selection | string or array |
| Checkbox | Multiple choices | array |
| True/False | Yes/no toggle | boolean |
| Relationship | Link to other posts | array of post objects |
Displaying ACF Fields in Templates
<?php
$price = get_field( 'property_price' );
$beds = get_field( 'bedrooms' );
$baths = get_field( 'bathrooms' );
$sqft = get_field( 'square_feet' );
$features = get_field( 'features' );
?>
<h1><?php the_title(); ?></h1>
<div class="property-meta">
<?php if ( $price ) : ?>
<span class="price">$<?php echo number_format( $price ); ?></span>
<?php endif; ?>
<?php if ( $beds ) : ?>
<span class="beds"><?php echo $beds; ?> Beds</span>
<?php endif; ?>
<?php if ( $baths ) : ?>
<span class="baths"><?php echo $baths; ?> Baths</span>
<?php endif; ?>
<?php if ( $sqft ) : ?>
<span class="sqft"><?php echo number_format( $sqft ); ?> sqft</span>
<?php endif; ?>
</div>
<div class="property-content">
<?php the_content(); ?>
</div>
<?php if ( $features ) : ?>
<ul class="features">
<?php foreach ( $features as $feature ) : ?>
<li><?php echo esc_html( $feature['feature_item'] ); ?></li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
For a repeater field named "features" with a sub-field "feature_item", get_field('features') returns an array of rows. Each row is an array of sub-field values.
Flexible Content Field
Flexible Content is a layout Builder. Each layout is a set of sub-fields. The editor chooses which layouts to include and in what order:
<?php if ( have_rows( 'content_blocks' ) ) : ?>
<?php while ( have_rows( 'content_blocks' ) ) : the_row(); ?>
<?php if ( get_row_layout() == 'text_block' ) : ?>
<div class="text-block">
<?php the_sub_field( 'content' ); ?>
</div>
<?php elseif ( get_row_layout() == 'image_block' ) : ?>
<div class="image-block">
<?php $image = get_sub_field( 'image' ); ?>
<img src="<?php echo esc_url( $image['url'] ); ?>"
alt="<?php echo esc_attr( $image['alt'] ); ?>">
</div>
<?php elseif ( get_row_layout() == 'call_to_action' ) : ?>
<div class="cta-block">
<h2><?php the_sub_field( 'heading' ); ?></h2>
<a href="<?php the_sub_field( 'button_url' ); ?>"
class="button">
<?php the_sub_field( 'button_text' ); ?>
</a>
</div>
<?php endif; ?>
<?php endwhile; ?>
<?php endif; ?>
Manual Meta Boxes Without ACF
If you prefer not to use a plugin, build meta boxes manually with add_meta_box() and save_post:
function dodatech_add_property_meta_boxes() {
add_meta_box(
'property_details',
'Property Details',
'dodatech_property_details_html',
'property',
'normal',
'high'
);
}
add_action( 'add_meta_boxes', 'dodatech_add_property_meta_boxes' );
function dodatech_property_details_html( $post ) {
wp_nonce_field( 'property_save', 'property_nonce' );
$price = get_post_meta( $post->ID, 'property_price', true );
$beds = get_post_meta( $post->ID, 'bedrooms', true );
$baths = get_post_meta( $post->ID, 'bathrooms', true );
?>
<table class="form-table">
<tr>
<th><label for="property_price">Price</label></th>
<td>
<input type="number" id="property_price" name="property_price"
value="<?php echo esc_attr( $price ); ?>" class="regular-text">
</td>
</tr>
<tr>
<th><label for="bedrooms">Bedrooms</label></th>
<td>
<input type="number" id="bedrooms" name="bedrooms"
value="<?php echo esc_attr( $beds ); ?>" class="small-text">
</td>
</tr>
<tr>
<th><label for="bathrooms">Bathrooms</label></th>
<td>
<input type="number" id="bathrooms" name="bathrooms"
value="<?php echo esc_attr( $baths ); ?>" step="0.5" class="small-text">
</td>
</tr>
</table>
<?php
}
function dodatech_save_property_meta( $post_id ) {
if ( ! isset( $_POST['property_nonce'] ) ) return;
if ( ! wp_verify_nonce( $_POST['property_nonce'], 'property_save' ) ) return;
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) return;
if ( ! current_user_can( 'edit_post', $post_id ) ) return;
$fields = array( 'property_price', 'bedrooms', 'bathrooms' );
foreach ( $fields as $field ) {
if ( isset( $_POST[ $field ] ) ) {
update_post_meta(
$post_id,
$field,
sanitize_text_field( $_POST[ $field ] )
);
}
}
}
add_action( 'save_post', 'dodatech_save_property_meta' );
Meta Query in WP_Query
Once you have custom fields, you need to query posts by their values. meta_query in WP_Query is how you filter:
$args = array(
'post_type' => 'property',
'meta_query' => array(
array(
'key' => 'property_price',
'value' => 500000,
'compare' => '<=',
'type' => 'NUMERIC',
),
array(
'key' => 'bedrooms',
'value' => 3,
'compare' => '>=',
'type' => 'NUMERIC',
),
),
'orderby' => 'meta_value_num',
'meta_key' => 'property_price',
'order' => 'DESC',
);
$query = new WP_Query( $args );
Comparison operators: =, !=, >, >=, <, <=, LIKE, NOT LIKE, IN, NOT IN, BETWEEN, NOT BETWEEN, EXISTS, NOT EXISTS.
The type parameter casts the value for proper comparison: NUMERIC, BINARY, CHAR, DATE, DATETIME, DECIMAL, SIGNED, TIME, UNSIGNED.
Metabox.io Plugin
Metabox.io is a popular alternative to ACF. It offers a similar field group system but with a different approach:
- Fields are registered in PHP code (no UI by default, though MB Custom Table UI exists)
- Supports custom database tables instead of wp_postmeta
- Integrates with Gutenberg and the API
- Lighter than ACF Pro but requires more coding
Common Mistakes
Using the wrong compare operator for the data type. Comparing a numeric
meta_valuewithLIKEor without specifyingtype => 'NUMERIC'produces alphabetical sorting. "100" comes before "9" alphabetically. Always settypefor numeric comparisons.Creating too many meta queries. Each meta_query clause adds a JOIN to the SQL query. Five meta_query clauses means five JOINs. For large sites, this kills performance. Consider using a custom table or an indexing plugin.
Forgetting to sanitize meta values on save.
update_post_metastores raw data. If a user enters<script>alert(1)</script>in a meta field and you output it withoutesc_html(), you create an XSS vulnerability. Always sanitize input and escape output.Making taxonomies hierarchical when flat is better. Product brands are flat (Nike, Adidas, Puma). Geographic locations are hierarchical (Country > State > City). Choosing wrong forces users to manage unnecessary parent/child relationships.
Not checking if ACF is active. If your theme depends on ACF, check
function_exists('get_field')before calling ACF functions. Otherwise, a user who deactivates ACF gets a white screen. Provide graceful fallbacks or a notice.
Practice Questions
- What is the difference between hierarchical and non-hierarchical taxonomies in terms of database storage, admin UI, and archive URL structure?
- Why does
meta_querywithcompare => 'LIKE'fail for numeric comparisons? - What are the three minimum security measures every save_post callback must implement?
Challenge: Create a "Recipe" CPT with a "Cuisine" hierarchical taxonomy and a "Diet" non-hierarchical taxonomy. Add custom fields for "Prep Time", "Cook Time", "Ingredients" (repeater), and "Difficulty" (select: easy, medium, hard). Build a template that displays all fields and taxonomy terms. Create a WP_Query that filters recipes by: "Italian cuisine AND easy difficulty AND prep time under 30 minutes".
FAQ
Mini Project
Build a "Product" CPT with complete taxonomy and field system:
- Register
productCPT with title, editor, thumbnail - Register
brand(non-hierarchical) anddepartment(hierarchical) taxonomies - Create ACF field group or manual meta box with: Price (number), Sale Price (number), SKU (text), Stock Status (true/false), and Features (repeater)
- Create archive-product.php with grid display, filterable by department and brand
- Create single-product.php showing price, sale price, SKU, stock status
- Use meta_query in a sidebar widget showing "Products on Sale" (sale_price is not empty)
- Order archive by price ascending using
meta_value_num
What's Next
Now that you can organize content with taxonomies and enhance it with fields, learn to query all of it efficiently with WP_Query:
Continue to Lesson 40: WP_Query and The Loop — Master custom database queries with every parameter explained.
Related lessons:
- Custom Post Types — Create the content containers taxonomies and fields enhance
- Hooks — Hook into save to auto-populate fields or send notifications
- WordPress REST API — Expose your taxonomies and fields via API endpoints
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro