WordPress WP_Term_Query: Query Taxonomy Terms by Meta, Hierarchy, and Custom Order

WordPress’s WP_Term_Query class is the taxonomy equivalent of WP_Query for posts — it provides a structured, filterable, cached API for querying taxonomy terms with fine-grained control over ordering, meta conditions, hierarchy depth, and what fields are returned. The older get_terms() function is a thin wrapper around WP_Term_Query and is appropriate for simple term retrieval. For complex queries — filtering by term meta, excluding certain terms, ordering by a custom meta value, getting only direct children of a term, or retrieving term IDs for a WHERE IN sub-query — working directly with WP_Term_Query gives you access to the full argument set and the parsed query object for debugging.

Problem: A shop page needs to display product categories sorted by a custom sort_order term meta value, with empty categories hidden, limited to the top-level (not subcategories), and only returning the term ID, name, and slug — not full WP_Term objects — for efficiency.

Solution: Use WP_Term_Query with meta_key for ordering, hide_empty, parent for top-level only, and fields to limit returned data.

<?php
// ── Basic WP_Term_Query ────────────────────────────────────────────────
$term_query = new WP_Term_Query( [
    'taxonomy'   => 'product_cat',
    'hide_empty' => true,            // exclude terms with no published posts
    'parent'     => 0,               // top-level only (direct children of root)
    'orderby'    => 'meta_value_num',// order by numeric meta value
    'meta_key'   => 'sort_order',    // the meta key to order by
    'order'      => 'ASC',
    'fields'     => 'all',           // 'all' | 'ids' | 'names' | 'id=>name' | 'id=>slug'
] );

$terms = $term_query->get_terms(); // array of WP_Term objects (with 'all')

foreach ( $terms as $term ) {
    printf( '<a href="%s">%s (%d)</a>',
        esc_url( get_term_link( $term ) ),
        esc_html( $term->name ),
        (int) $term->count
    );
}

// ── Get only IDs (efficient for use in queries) ────────────────────────
$cat_ids = ( new WP_Term_Query( [
    'taxonomy'   => 'product_cat',
    'hide_empty' => false,
    'fields'     => 'ids',           // returns [ 3, 7, 12, 15 ]
] ) )->get_terms();

// ── Filter by term meta ────────────────────────────────────────────────
$featured_terms = new WP_Term_Query( [
    'taxonomy'  => 'product_cat',
    'hide_empty' => true,
    'meta_query' => [
        [
            'key'     => 'featured',
            'value'   => '1',
            'compare' => '=',
        ],
    ],
] );

// ── Get all children of a specific term ───────────────────────────────
$electronics_id = 7;
$children = new WP_Term_Query( [
    'taxonomy'    => 'product_cat',
    'parent'      => $electronics_id, // direct children only
    'hide_empty'  => false,
    'orderby'     => 'name',
    'order'       => 'ASC',
] );

// ── Exclude specific terms ─────────────────────────────────────────────
$all_except_uncategorized = new WP_Term_Query( [
    'taxonomy' => 'category',
    'exclude'  => [ 1 ],             // term IDs to skip
    'fields'   => 'id=>name',        // [ 3 => 'WordPress', 5 => 'PHP', ... ]
] );

// ── get_terms() — wrapper, same arguments ─────────────────────────────
// Equivalent to: (new WP_Term_Query(...))->get_terms()
$terms = get_terms( [
    'taxonomy'   => 'category',
    'hide_empty' => true,
    'number'     => 10,
    'orderby'    => 'count',
    'order'      => 'DESC',
] );

NOTE: The 'fields' argument dramatically affects performance — 'ids' returns a plain array of integers with no object instantiation overhead, while 'all' (the default) constructs full WP_Term objects with all properties. Use 'ids' whenever you only need term IDs for further queries, and 'id=>name' or 'id=>slug' for dropdown population. The 'number' argument defaults to 0 (no limit) — always set an explicit limit in production queries to avoid retrieving thousands of terms in large sites. The 'childless' argument (set to true) is useful for returning only leaf-node terms — terms that have no children — which is helpful for faceted filtering UIs.