Add a custom admin column to the WooCommerce orders table

Custom admin posts column tutorial for WordPress

In this tutorial we’re going to add two custom columns to the WooCommerce admin orders table. We’ll add flags for each order’s shipping & billing countries, and it’s easy to add more columns with your own custom data… and all without installing another plugin. The code can be tweaked to work with different post types (posts, pages, etc).

Custom admin columns

The admin-orders table in WooCommerce is really just a WordPress admin-posts table for the “shop_orders” post type. So what we need to do is:

  • Add our columns to the list of available columns (which is just an array)
  • Render something useful in the custom columns for each order (each table row)

…and we use a couple of standard WordPress hooks to do this.

Example custom columns in the WooCommerce admin orders table
Custom admin columns
HookHow it works
This filter lets us modify the list of columns available on the admin screens (e.g. edit posts, edit pages, edit shop orders, etc.). In our case, the value of $screen->id is the same as the post type, “shop_order”. This means the actual filter hook we’re going to use is:
This action is triggered for each post in an admin table when a custom column needs to be rendered. Because $post->post_type is “shop_order”, the action we need to hook is:
Action and filter for adding custom columns

Scaffold the code

In your custom WordPress child theme, create a file called “wpt-admin-order-table-columns.php” and paste the following into it:


 * Headwall WP Tutorials Admin Order Table Columns (WPTAOTC)

defined('WPINC') || die();

const WPTAOTC_SHIPPING_COUNTRY_COLUMN_NAME = 'shipping_country';
const WPTAOTC_BILLING_COUNTRY_COLUMN_NAME = 'billing_country';
// Add more custom column name constants in here...
// ...

 * Enqueue admin styles to support our custom columns.
function wptaotc_admin_enqueue_scripts() {
	$base_uri = get_stylesheet_directory_uri();
	$theme_version = wp_get_theme()->get('Version');

		$base_uri . '/wpt-admin-order-table-columns.css',
add_action('admin_enqueue_scripts', 'wptaotc_admin_enqueue_scripts');

 * A utility function to inject an associative array into an existing
 * associative array, after a specific index.
function wptaotc_insert_into_array_after_key(array $source_array, string $key, array $new_element) {
	if (array_key_exists($key, $source_array)) {
		$position = array_search($key, array_keys($source_array)) + 1;
	} else {
		$position = count($source_array);

	$before = array_slice($source_array, 0, $position, true);
	$after = array_slice($source_array, $position, null, true);
	return array_merge($before, $new_element, $after);

 * Convert two-character country codes like "gb", "us", "de", "in" into an
 * HTML snippet for the country's flag. Cache snippets in $wptaotc_flag_htmls
 * so if we repeatedly request snippets for the same country, it should make
 * things a bit faster.
function wptaotc_get_flag_html(string $country_code) {
	global $wptaotc_flag_htmls;

	if (is_null($wptaotc_flag_htmls)) {
		$wptaotc_flag_htmls = array();

	if (empty($country_code)) {
		// $country_code cannot be blank.
	} elseif (array_key_exists($country_code, $wptaotc_flag_htmls)) {
		// We've already created the HTML for this country_code
		// and stored it in $wptaotc_flag_htmls
	} else {
		$wptaotc_flag_htmls[$country_code] = sprintf(
			'<span class="country-flag"><img src="%s/country-flags/%s.svg" /></span>',

	$html = null;
	if (array_key_exists($country_code, $wptaotc_flag_htmls)) {
		$html = $wptaotc_flag_htmls[$country_code];

	return $html;

Then edit your child theme’s “functions.php” and add the following couple of lines somewhere near the top:

// Headwall WP Tutorials Admin Order Table Columns
require_once dirname(__FILE__) . '/wpt-admin-order-table-columns.php';

What’s in the code so far

Right now the code only contains some utility functions, with a couple of control parameters (constants) at the top.

  • wptaotc_admin_enqueue_scripts()
    This is triggered by the admin_enqueue_scripts action, so our stylesheet will only be enqueued to admin pages.
  • wptaotc_insert_into_array_after_key()
    A handy utility function that makes it easy to inject one associative array into another, after a specified key. This is how we’ll inject our custom columns into the default list of columns at the position we want (rather than just append them to the end).
  • wptaotc_get_flag_html()
    Given a two-character country code like “gb” or “de”, return a HTML snippet for that country’s flag.

Supporting assets

Flag images

The Accurate country flags GitHub project has some nice open-source SVG flags, so lets grab those.

Go to the project page then download and extract the repository.

Accurate Country Flags
Download country flag SVGs from GitHub
Accurate Country Flag image files

Create a folder in your custom child theme called “country-flags” and copy all the SVG files into it. There should be around 255 image files in there.

Admin stylesheet

In your custom child theme, create a file called “wpt-admin-order-table-columns.css” and paste the following into it:

 * wpt-admin-order-table-countries.css

@media( min-width: 768px ) {
	.post-type-shop_order .wp-list-table th.column-billing_country,
	.post-type-shop_order .wp-list-table td.billing_country,
	.post-type-shop_order .wp-list-table th.column-shipping_country,
	.post-type-shop_order .wp-list-table td.shipping_country {
		text-align: center;
		width: 4em;

td.billing_country .country-flag,
td.shipping_country .country-flag {
	display: inline-block;
	width: 3.2em;
	height: 2em;
	border-radius: 0.4em;
	overflow: hidden;
	border:  1px solid grey;

td.billing_country .country-flag img,
td.shipping_country .country-flag img {
	width: 100%;
	height: 100%;
	margin: 0;
	object-fit: cover;

Core functionality

Now the utility functions, supporting CSS and flag SVG files are in place, we can add our core functionality. First, let’s modify the list of columns available on the “edit-shop_order” admin page. Add the following snippet to the end of the PHP file, “wpt-admin-order-table-columns.php“:

 * Modify the list of available columns for the posts table on the
 * shop_orders page.
function wptaotc_add_custom_columns($columns) {
	$columns = wptaotc_insert_into_array_after_key(
		'order_date', // Inject our columns after the "order_date" column
			// You can add more custom columns in here...
			// ...

	return $columns;
add_filter('manage_edit-shop_order_columns', 'wptaotc_add_custom_columns', 10, 1);

Save that, go to your admin orders page and reload it. You should see there are two new columns called “Billing” and “Shipping”.

Next we can add the function that renders the flags for each order (table row) – append the following snippet.

 * This gets called for each row in the orders table, for each custom column.
function wptaotc_render_custom_columns($column, $post_id) {
	if (empty($column)) {
		// Weird - this should bever happen.
	} elseif (!is_a($wc_order = wc_get_order($post_id), 'WC_Order')) {
		// Weird - this should bever happen.
	} else {
		switch ($column) {

			echo wptaotc_get_flag_html($wc_order->get_billing_country());

			echo wptaotc_get_flag_html($wc_order->get_shipping_country());

		// Add case statements for your custom columns in here...
		// ...

			// Unhandled custom column.
add_action('manage_shop_order_posts_custom_column', 'wptaotc_render_custom_columns', 10, 2);

When WordPress renders the admin orders table, our wptaotc_render_custom_columns() function gets called many times… for each row it’s called for every custom column, so whatever code we put in here needs to be efficient/fast. The code is easy to read-through and extend with your own columns.

The neat stuff actually happens inside wptaotc_get_flag_html():

If we assume that the act of making an HTML snippet for a country flag is potentially slow (to execute), then we don’t want to do it multiple times if we can avoid it. So when we make an HTML snippet for country’s flag, we store it in a global array. That way, if we need the HTML snippet again (for another row in the table) we can reuse the snippet we’ve already created. Here’s how the logic works inside wptaotc_get_flag_html():

  • Make sure the global variable $wptaotc_flag_htmls is an array
  • If $country_code is specified and it’s not already held in the $wptaotc_flag_htmls array, then…
    • Create the HTML snippet for the relevant flag and store it in $wptaotc_flag_htmls
  • If $country_code is specified and it is in the $wptaotc_flag_htmls array, then…
    • return the HTML snippet for $country_code from the $wptaotc_flag_htmls array

Wrapping up

Adding custom admin columns to back-end WordPress post tables is pretty easy. Hook one filter with a function to insert your custom column(s) into the array of available columns. Then hook an action to render your custom column(s) for each row in the table.

Try to keep the code in your column-rendering function simple, because as soon as you’ve got two or more custom columns, the function will become big and complicated.

The wptaotc_insert_into_array_after_key() and wptaotc_get_flag_html() functions are useful utility functions – the sort of thing you can use and reuse in multiple projects.

That’s it – have fun customising your WordPress admin post tables 😎👍

Like This Tutorial?

Let us know

WordPress plugins for developers

Leave a comment