Display taxonomy terms as a card grid in WordPress: a block you can build today

· 5 min read

When you build a WordPress site with custom post types, you often want a page that shows your top-level categories as visual cards: a projects page where each project type is a tile, a services page where each service area links to its archive, a recipe site where cuisines are the entry point. Click a card, land on the archive for that term.

The block editor's Query Loop block cannot do this. It queries posts. Taxonomy terms are a separate data type, and the block has no built-in mode for iterating over them. The workarounds are a PHP template, a shortcode, a full page builder, or a widget that outputs a flat list with no layout control.

The plugin described here adds a Taxonomy Cards block to the Gutenberg editor. Select any registered taxonomy, set the number of columns, and the block renders all top-level terms as cards, each linking to the term archive. It is server-side rendered, picks up your theme's spacing, and adds nothing to your stylesheet.

What one build gives you

  • A server-side rendered Gutenberg block that displays any registered WordPress taxonomy as a card grid
  • Column control from one to four, with optional term description and post count per card
  • Top-level terms by default, with a parent term ID setting to display any term's direct children instead
  • Each card linked to the term archive URL, populated automatically from get_term_link
  • Automatic refresh: add or rename a term in the admin and it appears in the grid on the next page load

What it does not do

  • Custom images per term card; image support requires a follow-up prompt once the base block is working
  • WooCommerce product category featured images; the WooCommerce Products blocks handle that already
  • Pagination or lazy loading when a taxonomy has many terms
  • Interactive front-end filtering or keyword search across the term grid
  • Custom ordering beyond alphabetical and WordPress term order

The prompt

Loads into the composer so you can edit it first. Nothing is built, and nothing is charged, until you send it.

Build a WordPress plugin that registers a server-side-rendered Gutenberg block named 'steem/taxonomy-cards' with title 'Taxonomy Cards'. Create a block.json at the plugin root with name 'steem/taxonomy-cards', title 'Taxonomy Cards', category 'widgets', editorScript 'file:./build/index.js', and attributes: taxonomy (type string, default 'category'), parent_term (type integer, default 0), columns (type integer, default 3), show_description (type boolean, default true), show_count (type boolean, default false). In the main PHP file register the block on the init hook using register_block_type pointing to the block.json file, with a render_callback. The render callback should call get_terms with args: taxonomy from attributes, parent from the parent_term attribute, hide_empty false, orderby 'name'. If the result is a WP_Error or empty array, return a p element with 'No terms found.' wrapped in the block wrapper. Otherwise build the output: a div with class 'wp-block-steem-taxonomy-cards' and inline style '--columns:[columns value]', and for each WP_Term an article with class 'taxonomy-card' containing: an h3 with an anchor whose href is get_term_link and text is esc_html of term name; if show_description is true and term description is not empty, a p with esc_html of term description; if show_count is true, a span with class 'taxonomy-card__count' containing esc_html of term count followed by ' posts'. Return the full output string. Add an inline style via wp_add_inline_style on 'wp-block-library' for: .wp-block-steem-taxonomy-cards { display:grid; grid-template-columns:repeat(var(--columns,3),1fr); gap:1.5em; } .taxonomy-card { border:1px solid #ddd; padding:1em; border-radius:4px; } .taxonomy-card h3 { margin:0 0 0.5em; } .taxonomy-card__count { font-size:0.875em; color:#666; }. Write a src/index.js. In index.js import registerBlockType from @wordpress/blocks; import InspectorControls and useBlockProps from @wordpress/block-editor; import PanelBody, SelectControl, TextControl, RangeControl, ToggleControl, Placeholder from @wordpress/components; import apiFetch from @wordpress/api-fetch; import useState and useEffect from @wordpress/element. Call registerBlockType with the block name, using metadata from block.json, and an edit function that: uses useState for taxonomyOptions initialised to empty array; uses useEffect to call apiFetch on /wp/v2/taxonomies and on success set taxonomyOptions to Object.values(response).map(t => ({ label: t.name, value: t.slug })); renders a div with useBlockProps containing an InspectorControls with a PanelBody labelled 'Block Settings' holding SelectControl for taxonomy, TextControl for Parent Term ID (type number, parsed to integer on onChange), RangeControl for Columns min 1 max 4, ToggleControl for Show Description, ToggleControl for Show Count; and a Placeholder label='Taxonomy Cards' with a p element reading 'Showing ' + attributes.taxonomy + ' terms as a ' + attributes.columns + '-column grid.'. The save function returns null. Add a package.json with scripts: { "build": "wp-scripts build" } and devDependencies for @wordpress/scripts. Register all PHP hooks inside a function attached to plugins_loaded.

Build this pluginAbout 55 credits · the free plan includes enough for one

What the block does

The block accepts any taxonomy registered on your site: Category, Tag, or any custom taxonomy created by a plugin like ACF or registered in your theme. A SelectControl in the block inspector populates automatically from your site's registered taxonomies so there is nothing to type.

By default the block displays top-level terms only, those with no parent. A Parent Term ID setting lets you show the direct children of any specific term instead. This makes it possible to build drill-down navigation: a top-level grid linking to parent term archive pages, and then a matching block on each parent term page showing its children.

Each card shows the term name as a heading linked to the term archive. Two optional toggles add the term description and the count of posts assigned to the term. The number of columns is a range control from one to four.

The use case it solves best

Sites where visitors are expected to browse by category rather than by date. Portfolio sites where project types or industries are a custom taxonomy. Learning resource sites where topics or skill levels are the primary navigation layer. Recipe sites where cuisines or dietary labels appear on the homepage as entry points to the archive.

The block renders on the front end using PHP and get_terms, so it always reflects current data without manual updates. Add a new taxonomy term in the admin and it appears in the grid on the next page load.

Because it is server-side rendered it inherits your theme's layout tokens. The wrapper uses a CSS custom property for the column count, and the gap between cards uses the theme's spacing scale if your theme exposes one. No plugin stylesheet to override.

When to use something else

If you need a custom image per card that is separate from the term description, this plugin does not handle that. Most sites that need term images store them in term meta using a plugin like Yoast SEO's category image field or a dedicated term meta box. The prompt in the next section includes a follow-up for adding image support once the base block is working.

If you are running WooCommerce and want to display product categories with featured images, WooCommerce already ships a Product Categories block in the block editor with built-in image support and more styling options. Use that instead.

If you need the card grid to filter or sort interactively on the front end without a page reload, this is the wrong tool. The block renders a static list. A JavaScript-driven filter would require a different build.

How to build it

Paste the prompt below into Steem. Once the plugin is active, open any page or post in the block editor, search for 'Taxonomy Cards' in the block inserter, and add it. In the block inspector, choose your taxonomy from the dropdown, set the number of columns, and toggle description and count on or off. The block renders a live preview using the server-side callback.

If you also need a custom image stored in term meta, the follow-up message is: 'Add an optional image to the Taxonomy Cards block. In the inspector, add a TextControl for Image Meta Key with default value thumbnail_id. In the render callback, for each term call get_term_meta with that key, then get_post with the returned ID and wp_get_attachment_image_src, and if found render an img element above the term name with the full-size URL and the term name as alt text.'

Questions

Can I use this for WordPress's built-in post categories?

Yes. The taxonomy dropdown in the block inspector lists every registered taxonomy including the built-in Category and Tag taxonomies. Select whichever you need.

Why can't the Query Loop block do this?

The Query Loop block queries posts and their attached data. Taxonomy terms are a different data type stored in a different database table. The block has no mode for iterating over terms rather than posts.

Can I show child terms under a specific parent category?

Yes. Set the Parent Term ID field in the inspector to the numeric ID of the parent term. The block will show only that term's direct children. Leave it at 0 to show top-level terms.

Does this work with WooCommerce product categories?

Yes, but WooCommerce ships its own Product Categories block that supports featured images and more styling options. Use the WooCommerce block unless you specifically need something it does not provide.

Can I put two taxonomy grids on the same page?

Yes. Each instance of the block is independent. Insert it multiple times and configure each with a different taxonomy, parent term, or column count.

The prompt

Loads into the composer so you can edit it first. Nothing is built, and nothing is charged, until you send it.

Build a WordPress plugin that registers a server-side-rendered Gutenberg block named 'steem/taxonomy-cards' with title 'Taxonomy Cards'. Create a block.json at the plugin root with name 'steem/taxonomy-cards', title 'Taxonomy Cards', category 'widgets', editorScript 'file:./build/index.js', and attributes: taxonomy (type string, default 'category'), parent_term (type integer, default 0), columns (type integer, default 3), show_description (type boolean, default true), show_count (type boolean, default false). In the main PHP file register the block on the init hook using register_block_type pointing to the block.json file, with a render_callback. The render callback should call get_terms with args: taxonomy from attributes, parent from the parent_term attribute, hide_empty false, orderby 'name'. If the result is a WP_Error or empty array, return a p element with 'No terms found.' wrapped in the block wrapper. Otherwise build the output: a div with class 'wp-block-steem-taxonomy-cards' and inline style '--columns:[columns value]', and for each WP_Term an article with class 'taxonomy-card' containing: an h3 with an anchor whose href is get_term_link and text is esc_html of term name; if show_description is true and term description is not empty, a p with esc_html of term description; if show_count is true, a span with class 'taxonomy-card__count' containing esc_html of term count followed by ' posts'. Return the full output string. Add an inline style via wp_add_inline_style on 'wp-block-library' for: .wp-block-steem-taxonomy-cards { display:grid; grid-template-columns:repeat(var(--columns,3),1fr); gap:1.5em; } .taxonomy-card { border:1px solid #ddd; padding:1em; border-radius:4px; } .taxonomy-card h3 { margin:0 0 0.5em; } .taxonomy-card__count { font-size:0.875em; color:#666; }. Write a src/index.js. In index.js import registerBlockType from @wordpress/blocks; import InspectorControls and useBlockProps from @wordpress/block-editor; import PanelBody, SelectControl, TextControl, RangeControl, ToggleControl, Placeholder from @wordpress/components; import apiFetch from @wordpress/api-fetch; import useState and useEffect from @wordpress/element. Call registerBlockType with the block name, using metadata from block.json, and an edit function that: uses useState for taxonomyOptions initialised to empty array; uses useEffect to call apiFetch on /wp/v2/taxonomies and on success set taxonomyOptions to Object.values(response).map(t => ({ label: t.name, value: t.slug })); renders a div with useBlockProps containing an InspectorControls with a PanelBody labelled 'Block Settings' holding SelectControl for taxonomy, TextControl for Parent Term ID (type number, parsed to integer on onChange), RangeControl for Columns min 1 max 4, ToggleControl for Show Description, ToggleControl for Show Count; and a Placeholder label='Taxonomy Cards' with a p element reading 'Showing ' + attributes.taxonomy + ' terms as a ' + attributes.columns + '-column grid.'. The save function returns null. Add a package.json with scripts: { "build": "wp-scripts build" } and devDependencies for @wordpress/scripts. Register all PHP hooks inside a function attached to plugins_loaded.

Build this pluginAbout 55 credits · the free plan includes enough for one

Read next

Other plugins you can build this way

Each loads into the composer, ready to edit.