Type something to search...
How to Register Custom Taxonomies in WordPress?

How to Register Custom Taxonomies in WordPress?

A recipe site starts with categories like Breakfast, Dinner, and Desserts. Then the editors want to filter by cuisine, by diet, and by cooking time. Soon the category list holds "Italian", "Vegan", "Under 30 Minutes", and "Dinner" side by side, the hierarchy makes no sense, and nobody can build a clean "all vegan Italian dinners" page. The fix is not more categories. It is custom taxonomies: separate, purpose-built ways to group content, each with its own admin screen, URLs, and archive pages.

This article covers what a taxonomy is, how to register one with register_taxonomy(), every argument that matters in practice, how to make it work in the block editor, how to attach it to custom post types, how URLs and rewrite rules behave, how to build archive templates for classic and block themes, and how to query, display, and manage terms in code and with WP-CLI.

What Is a Taxonomy in WordPress?

A taxonomy is a way of grouping content. Each taxonomy holds terms, and each term can be assigned to many posts. WordPress ships with two public taxonomies you already know:

  • Categories (category): hierarchical, so terms can have parents and children
  • Tags (post_tag): flat, so every term sits at the same level

If you are unsure when to use each, the differences are covered in detail in what is the difference between categories and tags in WordPress.

A custom taxonomy is one you register yourself. It behaves exactly like categories or tags, but it only contains the terms that belong to its purpose. For the recipe site above, a sensible model looks like this:

TaxonomyTypeExample terms
CourseHierarchicalBreakfast, Lunch, Dinner, Dessert
CuisineHierarchicalItalian, Mexican, Indian → South Indian
DietFlatVegan, Gluten-Free, Keto
CategoryBuilt-inLeft for editorial sections only

Each taxonomy gets its own admin menu item, its own URL base such as /cuisine/italian/, and its own archive. Queries become simple: "posts in Cuisine: Italian AND Diet: Vegan" is a two-line tax_query.

When to use a taxonomy instead of a custom field

A common modeling question is whether a piece of data should be a taxonomy term or a custom field (post meta). A useful rule:

  • Use a taxonomy when many posts share the same value and you want to group, filter, or list posts by it. Cuisine, genre, location, and skill level are taxonomies.
  • Use a custom field when the value is unique or nearly unique per post, or numeric. Price, cooking time in minutes, ISBN, and event date are custom fields.

Taxonomy queries are indexed and fast. Filtering thousands of posts by a meta value is much slower, so the choice has performance consequences as well as editorial ones.

Registering Your First Custom Taxonomy

Taxonomies are registered with register_taxonomy() on the init hook. Put the code in a small plugin, not your theme, so the taxonomy and its terms survive a theme switch. A must-use plugin or a regular site plugin both work. For safe places to put code like this, see how to add custom code snippets to WordPress safely.

Here is a complete plugin that registers a hierarchical Cuisine taxonomy for posts:

<?php
// wp-content/plugins/recipe-taxonomies/recipe-taxonomies.php
/**
 * Plugin Name: Recipe Taxonomies
 * Description: Registers the Cuisine and Diet taxonomies.
 * Version:     1.0.0
 */

if ( ! defined( 'ABSPATH' ) ) {
exit;
}

function rt_register_cuisine_taxonomy() {
$labels = array(
'name'              => _x( 'Cuisines', 'taxonomy general name', 'recipe-taxonomies' ),
'singular_name'     => _x( 'Cuisine', 'taxonomy singular name', 'recipe-taxonomies' ),
'search_items'      => __( 'Search Cuisines', 'recipe-taxonomies' ),
'all_items'         => __( 'All Cuisines', 'recipe-taxonomies' ),
'parent_item'       => __( 'Parent Cuisine', 'recipe-taxonomies' ),
'parent_item_colon' => __( 'Parent Cuisine:', 'recipe-taxonomies' ),
'edit_item'         => __( 'Edit Cuisine', 'recipe-taxonomies' ),
'view_item'         => __( 'View Cuisine', 'recipe-taxonomies' ),
'update_item'       => __( 'Update Cuisine', 'recipe-taxonomies' ),
'add_new_item'      => __( 'Add New Cuisine', 'recipe-taxonomies' ),
'new_item_name'     => __( 'New Cuisine Name', 'recipe-taxonomies' ),
'not_found'         => __( 'No cuisines found.', 'recipe-taxonomies' ),
'back_to_items'     => __( '← Back to Cuisines', 'recipe-taxonomies' ),
'menu_name'         => __( 'Cuisines', 'recipe-taxonomies' ),
);

register_taxonomy(
'cuisine',
array( 'post' ),
array(
'labels'            => $labels,
'hierarchical'      => true,
'public'            => true,
'show_ui'           => true,
'show_admin_column' => true,
'show_in_rest'      => true,
'query_var'         => true,
'rewrite'           => array(
'slug'         => 'cuisine',
'with_front'   => false,
'hierarchical' => true,
),
)
);
}
add_action( 'init', 'rt_register_cuisine_taxonomy' );

Activate the plugin and a Posts → Cuisines menu appears. In the block editor, a Cuisines panel shows up in the post sidebar, and the list table gains a Cuisines column.

Choosing a taxonomy key

The first argument, cuisine, is the taxonomy key. It is stored in the database for every term, so choose it carefully:

  • Maximum 32 characters, lowercase letters, numbers, underscores, and dashes only.
  • Do not use reserved names. WordPress uses many query variables internally, and a taxonomy named type, author, year, name, page, order, term, or category_name will break queries in confusing ways. Prefixing, for example rt_cuisine, avoids collisions entirely, at the cost of a less pretty key. The rewrite slug can still be plain cuisine.
  • Do not rename it later. Changing the key orphans every existing term assignment until you migrate them with SQL or WP-CLI.

The register_taxonomy Arguments That Matter

register_taxonomy() accepts many arguments. These are the ones you will set on almost every project:

ArgumentDefaultWhat it does
hierarchicalfalsetrue behaves like categories, false like tags
publictrueControls front-end visibility and the defaults of several flags
publicly_queryableValue of publicWhether term archives and query vars work on the front end
show_uiValue of publicShows the admin screen and post editor panel
show_in_menuValue of show_uiAdds the submenu under the post type menu
show_in_nav_menusValue of publicLets terms be added to navigation menus
show_in_restfalseExposes the taxonomy to the REST API and the block editor
rest_baseTaxonomy keyChanges the REST route, for example /wp/v2/cuisines
show_admin_columnfalseAdds a column to the post list table
show_tagcloudValue of show_uiAllows the Tag Cloud block to use it
query_varTaxonomy keyEnables ?cuisine=italian style queries
rewritetruePretty permalinks for term archives
default_termNoneA term assigned automatically when none is chosen
capabilitiesCategory-like defaultsWhich capabilities manage, edit, delete, and assign terms
meta_box_cbBased on hierarchicalThe classic editor meta box; false hides it
sortNoneWhether to remember the order terms were added to a post

show_in_rest is not optional anymore

The block editor is a JavaScript application that talks to WordPress through the REST API. If show_in_rest is false, which is the default, your taxonomy will not appear in the block editor at all. Editors can only assign terms through Quick Edit in the post list. This is the most common reason a "registered" taxonomy seems to be missing. Set show_in_rest => true unless you deliberately want the taxonomy hidden from the editor and the API.

Hierarchical versus flat, in the editor

The hierarchical flag changes more than parent relationships. It also changes the editor UI:

  • Hierarchical taxonomies get a checkbox list, like categories. Editors pick from existing terms and can add new ones with a parent.
  • Flat taxonomies get a token field, like tags. Editors type terms and press Enter, and new terms are created on the fly.

Pick the UI your editors need. A controlled vocabulary such as Course works best as checkboxes. A free-form vocabulary such as ingredients works best as tokens.

Restricting who can manage terms

By default, anyone who can edit posts can assign terms, and anyone with manage_categories can create, edit, and delete them. If you want a fixed vocabulary that authors can assign but not extend, map custom capabilities:

<?php
// wp-content/plugins/recipe-taxonomies/recipe-taxonomies.php (inside the args array)
'capabilities' => array(
'manage_terms' => 'manage_options',
'edit_terms'   => 'manage_options',
'delete_terms' => 'manage_options',
'assign_terms' => 'edit_posts',
),

Now only administrators can change the list of cuisines, while authors and editors can still tag their recipes. If you need finer control over roles, see how to set up and manage user roles in WordPress.

Registering a Flat Taxonomy with a Default Term

Here is the second taxonomy for the recipe site, a flat Diet taxonomy, added to the same plugin. It also shows default_term, which creates a term and assigns it to posts saved without one:

<?php
// wp-content/plugins/recipe-taxonomies/recipe-taxonomies.php
function rt_register_diet_taxonomy() {
register_taxonomy(
'diet',
array( 'post' ),
array(
'labels'            => array(
'name'                       => __( 'Diets', 'recipe-taxonomies' ),
'singular_name'              => __( 'Diet', 'recipe-taxonomies' ),
'search_items'               => __( 'Search Diets', 'recipe-taxonomies' ),
'popular_items'              => __( 'Popular Diets', 'recipe-taxonomies' ),
'all_items'                  => __( 'All Diets', 'recipe-taxonomies' ),
'edit_item'                  => __( 'Edit Diet', 'recipe-taxonomies' ),
'add_new_item'               => __( 'Add New Diet', 'recipe-taxonomies' ),
'new_item_name'              => __( 'New Diet Name', 'recipe-taxonomies' ),
'separate_items_with_commas' => __( 'Separate diets with commas', 'recipe-taxonomies' ),
'add_or_remove_items'        => __( 'Add or remove diets', 'recipe-taxonomies' ),
'choose_from_most_used'      => __( 'Choose from the most used diets', 'recipe-taxonomies' ),
'not_found'                  => __( 'No diets found.', 'recipe-taxonomies' ),
'menu_name'                  => __( 'Diets', 'recipe-taxonomies' ),
),
'hierarchical'      => false,
'show_admin_column' => true,
'show_in_rest'      => true,
'rewrite'           => array( 'slug' => 'diet', 'with_front' => false ),
'default_term'      => array(
'name' => __( 'Any Diet', 'recipe-taxonomies' ),
'slug' => 'any-diet',
),
)
);
}
add_action( 'init', 'rt_register_diet_taxonomy' );

The default term behaves like "Uncategorized" for categories: it cannot be deleted from the admin while it is the default.

Attaching Taxonomies to Custom Post Types

The second argument of register_taxonomy() is the list of post types the taxonomy applies to. It can be built-in types, custom types, or both:

<?php
register_taxonomy( 'cuisine', array( 'post', 'recipe' ), $args );

When the post type is registered by another plugin, or you want to attach an existing taxonomy to a new type, use register_taxonomy_for_object_type() after both exist:

<?php
// wp-content/plugins/recipe-taxonomies/recipe-taxonomies.php
function rt_attach_taxonomies() {
register_taxonomy_for_object_type( 'cuisine', 'recipe' );
register_taxonomy_for_object_type( 'post_tag', 'recipe' );
}
add_action( 'init', 'rt_attach_taxonomies', 20 );

The priority of 20 runs after the default priority of 10, so the post type and taxonomy are already registered. The reverse also works: a post type can declare its taxonomies through the taxonomies argument of register_post_type(). If you have not built the post type yet, follow how to create a custom post type in WordPress first.

Rewrite Rules and Flushing Permalinks

With pretty permalinks enabled, rewrite turns term archives into URLs like /cuisine/italian/. With 'hierarchical' => true inside the rewrite array, child terms include their parents, such as /cuisine/indian/south-indian/. with_front => false ignores any static prefix in your permalink structure, so a /blog/%postname%/ structure does not produce /blog/cuisine/italian/.

WordPress caches rewrite rules in the database. A brand-new taxonomy has no rules until they are regenerated, which is why new term archives return a 404 Not Found at first. Two ways to fix it:

  • Manually: visit Settings → Permalinks and click Save Changes. You do not need to change anything.
  • In code: flush once on plugin activation, never on every request.
<?php
// wp-content/plugins/recipe-taxonomies/recipe-taxonomies.php
function rt_activate() {
rt_register_cuisine_taxonomy();
rt_register_diet_taxonomy();
flush_rewrite_rules();
}
register_activation_hook( __FILE__, 'rt_activate' );

function rt_deactivate() {
flush_rewrite_rules();
}
register_deactivation_hook( __FILE__, 'rt_deactivate' );

The activation function calls the registration functions directly because the init hook has already fired by the time the activation hook runs. Calling flush_rewrite_rules() on init for every page load is expensive and is a common performance mistake.

Displaying Terms on the Front End

In a block theme

Block themes display terms with the Terms block, which is a variation of the Post Terms block (core/post-terms). Open a single template in the Site Editor, insert Cuisines, and it lists the current post's cuisines as links. It only offers your taxonomy if show_in_rest is enabled.

For term archives, create a template file in the theme. WordPress checks the template hierarchy in this order:

  1. taxonomy-cuisine-italian.html (one specific term)
  2. taxonomy-cuisine.html (every term in the taxonomy)
  3. taxonomy.html
  4. archive.html
  5. index.html

Here is a minimal templates/taxonomy-cuisine.html with a Query Loop that inherits the archive's query:

<!-- wp-content/themes/my-theme/templates/taxonomy-cuisine.html -->
<!-- wp:template-part {"slug":"header"} /-->

<!-- wp:group {"tagName":"main","layout":{"type":"constrained"}} -->
<main class="wp-block-group">
  <!-- wp:query-title {"type":"archive"} /-->
  <!-- wp:term-description /-->

  <!-- wp:query {"query":{"inherit":true}} -->
  <div class="wp-block-query">
    <!-- wp:post-template -->
    <!-- wp:post-featured-image {"isLink":true} /-->
    <!-- wp:post-title {"isLink":true} /-->
    <!-- wp:post-excerpt /-->
    <!-- /wp:post-template -->

    <!-- wp:query-pagination -->
    <!-- wp:query-pagination-previous /-->
    <!-- wp:query-pagination-numbers /-->
    <!-- wp:query-pagination-next /-->
    <!-- /wp:query-pagination -->

    <!-- wp:query-no-results -->
    <!-- wp:paragraph -->
    <p>No recipes found for this cuisine yet.</p>
    <!-- /wp:paragraph -->
    <!-- /wp:query-no-results -->
  </div>
  <!-- /wp:query -->
</main>
<!-- /wp:group -->

<!-- wp:template-part {"slug":"footer"} /-->

You can also create the same template visually under Appearance → Editor → Templates → Add New Template, where your taxonomy appears in the list once it is public.

In a classic theme

Classic themes use the same hierarchy with PHP files: taxonomy-cuisine-italian.php, taxonomy-cuisine.php, taxonomy.php, archive.php, then index.php. To print a post's terms inside the loop, use get_the_term_list():

<?php
// wp-content/themes/my-child-theme/template-parts/content.php
$cuisines = get_the_term_list( get_the_ID(), 'cuisine', '<p class="post-cuisines">Cuisine: ', ', ', '</p>' );

if ( $cuisines && ! is_wp_error( $cuisines ) ) {
echo wp_kses_post( $cuisines );
}

If you are editing a third-party theme, put template changes in a child theme so updates do not overwrite them, as explained in creating and customizing WordPress child themes.

Querying Posts by Custom Taxonomy

The tax_query argument of WP_Query handles everything from a single term to complex combinations. This query returns vegan Italian recipes, including posts in child cuisines of Italian:

<?php
// wp-content/themes/my-child-theme/page-vegan-italian.php
$recipes = new WP_Query(
array(
'post_type'      => 'post',
'posts_per_page' => 12,
'tax_query'      => array(
'relation' => 'AND',
array(
'taxonomy'         => 'cuisine',
'field'            => 'slug',
'terms'            => array( 'italian' ),
'include_children' => true,
),
array(
'taxonomy' => 'diet',
'field'    => 'slug',
'terms'    => array( 'vegan' ),
),
),
)
);

if ( $recipes->have_posts() ) {
echo '<ul class="recipe-list">';
while ( $recipes->have_posts() ) {
$recipes->the_post();
printf(
'<li><a href="%s">%s</a></li>',
esc_url( get_permalink() ),
esc_html( get_the_title() )
);
}
echo '</ul>';
wp_reset_postdata();
}

Useful tax_query options:

  • field: term_id, slug, name, or term_taxonomy_id. Slugs are stable and readable, so they are the best choice in code.
  • operator: IN (default), NOT IN, AND (the post must have every listed term), EXISTS, and NOT EXISTS.
  • include_children: true by default for hierarchical taxonomies.

In a block theme, the Query Loop block offers the same filtering without code. Select the block, open Filters, and add Taxonomies. Every taxonomy registered with show_in_rest is listed.

Getting terms in PHP

A few functions cover nearly every need:

  • get_the_terms( $post_id, 'cuisine' ) returns the terms attached to one post, with caching.
  • get_terms( array( 'taxonomy' => 'cuisine', 'hide_empty' => true ) ) returns all terms in a taxonomy.
  • get_term_link( $term ) returns a term's archive URL.
  • wp_set_object_terms( $post_id, array( 'italian' ), 'cuisine', true ) assigns terms in code; the last argument appends instead of replacing.

Always check the return value with is_wp_error(), because these functions return a WP_Error if the taxonomy does not exist.

Adding Term Meta

Terms can store their own metadata, such as a color, an icon, or a featured image ID for each cuisine. Register the meta key so it is sanitized and available over REST:

<?php
// wp-content/plugins/recipe-taxonomies/recipe-taxonomies.php
function rt_register_term_meta() {
register_term_meta(
'cuisine',
'accent_color',
array(
'type'              => 'string',
'single'            => true,
'show_in_rest'      => true,
'sanitize_callback' => 'sanitize_hex_color',
)
);
}
add_action( 'init', 'rt_register_term_meta' );

Read it with get_term_meta( $term_id, 'accent_color', true ) and write it with update_term_meta(). Adding a field to the term edit screen requires the {$taxonomy}_edit_form_fields and edited_{$taxonomy} hooks, or a fields plugin if you prefer not to write form code.

Managing Terms with WP-CLI

Large vocabularies are faster to manage from the command line. These are standard WP-CLI commands:

# Terminal
wp taxonomy list --fields=name,label,hierarchical,public
wp term create cuisine "Italian" --slug=italian
wp term create cuisine "South Indian" --slug=south-indian --parent=$(wp term get cuisine indian --by=slug --field=term_id)
wp term list cuisine --fields=term_id,name,slug,parent,count
wp post term add 123 diet vegan gluten-free
wp term delete diet keto --by=slug

For a full tour of what WP-CLI can do, see how to use WP-CLI to manage WordPress from the command line.

Registering Taxonomies Without Code

If you prefer a UI, plugins such as Custom Post Type UI and Advanced Custom Fields can register taxonomies from the admin. They call the same register_taxonomy() function with the settings you choose, and most can export the generated PHP. That export is a good way to learn the arguments, and moving it into your own plugin later removes the dependency.

Common Problems and Fixes

  • The taxonomy does not appear in the block editor. show_in_rest is missing or false. Set it to true. Also confirm the taxonomy is attached to the post type you are editing.
  • Term archives return 404. Rewrite rules were not flushed after registration. Save Settings → Permalinks once, or flush in an activation hook.
  • Queries return nothing or the wrong posts. The taxonomy key collides with a reserved query variable such as type or author. Rename the key, or use a prefixed key with a clean rewrite slug.
  • Terms vanish after switching themes. The taxonomy was registered in the old theme's functions.php. The terms are still in the database; move the registration into a plugin and they reappear.
  • The column or Quick Edit field is missing. Set show_admin_column => true and keep show_ui enabled.
  • Child term URLs are flat. Add 'hierarchical' => true inside the rewrite array, not just at the top level, then flush permalinks.

Custom Taxonomies FAQ

Use a plugin. Taxonomies describe your content, not its design, so they should survive a theme change. If you register them in a theme, switching themes hides the taxonomy and its term archives until you register it again, even though the terms remain in the database.

The block editor only shows taxonomies that are exposed to the REST API. Add show_in_rest set to true in the register_taxonomy arguments and make sure the taxonomy is attached to the post type you are editing.

A hierarchical taxonomy lets terms have parents and children and shows a checkbox list in the editor, like categories. A flat taxonomy has no parent relationships and shows a token field where editors type terms, like tags.

Yes. Pass an array of post types as the second argument to register_taxonomy, or attach the taxonomy later with register_taxonomy_for_object_type. Term counts and archives then include content from every attached post type.

There is no hard limit, but every taxonomy adds a panel to the editor and an admin screen. Create a taxonomy only when editors or visitors will actually group or filter content by it. Values that are unique per post belong in custom fields instead.

Term archives are indexable pages with their own URLs, so they can rank for category-style searches if they have enough content and a description. Thin archives with one or two posts can be set to noindex in your SEO plugin until they grow.

Conclusion

Custom taxonomies turn a messy category list into a clean content model. Register each one with register_taxonomy() on init, inside a plugin, with a short and non-reserved key. Set show_in_rest so it appears in the block editor, choose hierarchical based on the editing experience you want, add show_admin_column for quick scanning, and flush rewrite rules once on activation so archives work immediately.

From there, build archive templates through the template hierarchy, filter content with tax_query or the Query Loop block, store extra data in term meta, and manage large vocabularies with WP-CLI. The result is content that editors can organize consistently and visitors can browse the way they think.

Here are some useful references for going deeper on custom taxonomies:

  1. WordPress Developer Resources: register_taxonomy() — the full list of arguments and labels.
  2. WordPress Plugin Handbook: Working with Custom Taxonomies — registration, usage, and examples.
  3. WordPress Theme Handbook: Template Hierarchy — how taxonomy archive templates are chosen.
  4. WordPress Developer Resources: WP_Query taxonomy parameters — every tax_query option.
  5. WP-CLI Commands: wp term — create, list, update, and delete terms from the command line.
Tags :
Share :

Related Posts

WordPress optimization with specific recommended approach

WordPress optimization with specific recommended approach

Whether you run a high traffic WordPress installation or a small blog on a low cost shared host, you should optimize WordPress and your server to run

Continue Reading
Creating and Customizing WordPress Child Themes

Creating and Customizing WordPress Child Themes

Creating a child theme in WordPress is a best practice for making modifications to a theme. By using a child theme, you can update the parent theme w

Continue Reading
Understanding the Distinction Categories vs. Tags in WordPress

Understanding the Distinction Categories vs. Tags in WordPress

WordPress, a powerful content management system, offers a plethora of features to organize content effectively. Among these features, categories and

Continue Reading