WordPress How-To: Add Custom Columns to Admin Post Lists
Boost WordPress admin efficiency with custom columns
Originally published: November 28th, 2017. Updated on: April 29th, 2025.
I often find myself needing to display additional data alongside the default Post listings in the WordPress admin area (wp-admin
). While WordPress doesn't offer as many layout hooks for the admin list tables as it does for the front-end or post editor, there are more than enough to get the job done for adding custom columns. It's quite flexible, really.
(Image placeholder: Default WordPress Posts admin table)
The goal here is to insert a couple of new columns into the standard "Posts" list table. We'll look at two key action/filter hooks:
manage_{$post_type}_posts_columns
: Filters the columns displayed for a post type. Used to add, remove, or reorder column headers.manage_{$post_type}_posts_custom_column
: Action hook fired for each custom column, allowing you to output its content.
Using these, we can add useful information at a glance. Sure, plugins exist that offer similar functionality, but they can be limited, and I usually prefer small code snippets for specific tasks like this to keep things tidy and avoid unnecessary plugin overhead.
Example 1: Adding a ‘Word Count’ Column
Depending on the site, a word count column might be useful. Let's add one that counts words in the main post content. (Counting words from custom fields would require more creative coding).
Step 1: Add the Column Header
We use the manage_posts_posts_columns
filter (note the _posts
part, specifically for the 'post' post type) to add our new column header. Let's insert it just before the default 'Tags' column.
<?php
/**
* Add 'Word Count' column header to the Posts list table.
*
* @param array $columns Existing columns.
* @return array Modified columns.
*/
function my_plugin_add_word_count_column_header(array $columns): array
{
// Define the new column
$new_column = ['word_count' => __('Word Count', 'your-text-domain')];
// Specify where to insert it (before 'tags')
$insert_before_key = 'tags';
// Find the position of the 'tags' column
$tag_column_position = array_search($insert_before_key, array_keys($columns), true);
// If 'tags' column exists, insert before it
if ($tag_column_position !== false) {
$columns = array_slice($columns, 0, $tag_column_position, true) +
$new_column +
array_slice($columns, $tag_column_position, null, true);
} else {
// Otherwise, just add it to the end (before 'date')
$date_column_position = array_search('date', array_keys($columns), true);
if ($date_column_position !== false) {
$columns = array_slice($columns, 0, $date_column_position, true) +
$new_column +
array_slice($columns, $date_column_position, null, true);
} else {
// Fallback: add to the very end
$columns = array_merge($columns, $new_column);
}
}
return $columns;
}
add_filter('manage_posts_posts_columns', 'my_plugin_add_word_count_column_header');
?>
Step 2: Populate the Column Content
Now, we use the manage_posts_posts_custom_column
action hook to output the data for our word_count
column.
<?php
/**
* Display content for the custom 'Word Count' column.
*
* @param string $column_name The name of the column.
* @param int $post_id The ID of the current post.
*/
function my_plugin_display_word_count_column_content(string $column_name, int $post_id): void
{
// Check if it's our custom column
if ('word_count' === $column_name) {
$post = get_post($post_id);
if ($post) {
// Get post content
$content = $post->post_content;
// Basic word count - counts spaces. Use strip_tags to avoid counting HTML.
// For more accuracy, regex or mb_split might be better.
$word_count = str_word_count(strip_tags($content));
echo esc_html($word_count);
} else {
echo '—'; // Indicate error or missing post
}
}
}
add_action('manage_posts_posts_custom_column', 'my_plugin_display_word_count_column_content', 10, 2);
?>
Example 2: Listing All Contributors to an Article
WordPress's revisions feature tracks changes made by multiple users. We can use this data (wp_get_post_revisions()
) to list everyone who has contributed to a post, not just the original author.
Step 1: Add the Column Header (and Remove 'Author')
Let's add a 'Contributors' column and remove the default 'Author' column since it becomes somewhat redundant. We'll place 'Contributors' before the 'Categories' column.
<?php
/**
* Add 'Contributors' column and remove 'Author' column from Posts list table.
*
* @param array $columns Existing columns.
* @return array Modified columns.
*/
function my_plugin_add_contributors_column_header(array $columns): array
{
// 1. Remove the default 'Author' column
unset($columns['author']);
// 2. Define the new 'Contributors' column
$new_column = ['contributors' => __('Contributors', 'your-text-domain')];
// 3. Specify where to insert it (before 'categories')
$insert_before_key = 'categories';
// Find the position of the 'categories' column
$category_column_position = array_search($insert_before_key, array_keys($columns), true);
// If 'categories' column exists, insert before it
if ($category_column_position !== false) {
$columns = array_slice($columns, 0, $category_column_position, true) +
$new_column +
array_slice($columns, $category_column_position, null, true);
} else {
// Fallback: add it before the 'date' column
$date_column_position = array_search('date', array_keys($columns), true);
if ($date_column_position !== false) {
$columns = array_slice($columns, 0, $date_column_position, true) +
$new_column +
array_slice($columns, $date_column_position, null, true);
} else {
// Fallback: add to the very end
$columns = array_merge($columns, $new_column);
}
}
return $columns;
}
// Note: Use a different priority if combining with the word count filter,
// or combine the logic into a single function.
add_filter('manage_posts_posts_columns', 'my_plugin_add_contributors_column_header', 11); // Higher priority
?>
Step 2: Populate the Column Content
We hook into manage_posts_posts_custom_column
again. This time, we fetch revisions, get the unique author IDs, and display links to their user edit profiles.
<?php
/**
* Display content for the custom 'Contributors' column.
*
* @param string $column_name The name of the column.
* @param int $post_id The ID of the current post.
*/
function my_plugin_display_contributors_column_content(string $column_name, int $post_id): void
{
if ('contributors' === $column_name) {
$author_ids = [];
// Get the original post author first
$post = get_post($post_id);
if ($post && isset($post->post_author)) {
$author_ids[] = (int) $post->post_author;
}
// Get authors from revisions
$revisions = wp_get_post_revisions($post_id, ['fields' => 'post_author']); // More efficient
if (!empty($revisions)) {
foreach ($revisions as $revision) {
if (isset($revision->post_author)) {
$author_ids[] = (int) $revision->post_author;
}
}
}
// Get unique author IDs
$unique_author_ids = array_unique($author_ids);
$author_links = [];
if (!empty($unique_author_ids)) {
foreach ($unique_author_ids as $author_id) {
$user_info = get_userdata($author_id);
if ($user_info) {
$display_name = $user_info->display_name;
// Link to user's edit page if current user can edit users
if (current_user_can('edit_users')) {
$link = get_edit_user_link($author_id);
$author_links[] = sprintf(
'<a href="%s">%s</a>',
esc_url($link),
esc_html($display_name)
);
} else {
// Otherwise, just display the name
$author_links[] = esc_html($display_name);
}
}
}
}
if (!empty($author_links)) {
echo implode(', ', $author_links);
} else {
echo '—'; // No authors found
}
}
}
add_action('manage_posts_posts_custom_column', 'my_plugin_display_contributors_column_content', 10, 2);
?>
End Result:
After adding both sets of code (or the combined version below), your Posts list table should look something like this:
(Image placeholder: Modified WordPress Posts admin table showing 'Word Count' and 'Contributors' columns)
The Code in Full (Combined and Optimized)
Instead of separate functions for each column, it's cleaner to combine the logic. You know, keep things tidy.
<?php
// --- Column Headers ---
/**
* Modify the Posts list table column headers.
* Adds 'Word Count' and 'Contributors', removes 'Author'.
*
* @param array $columns Existing columns.
* @return array Modified columns.
*/
function my_plugin_modify_post_table_headers(array $columns): array
{
$new_columns = [];
foreach ($columns as $key => $title) {
// Add Contributors before Categories
if ($key === 'categories') {
$new_columns['contributors'] = __('Contributors', 'your-text-domain');
}
// Add Word Count before Tags
if ($key === 'tags') {
$new_columns['word_count'] = __('Word Count', 'your-text-domain');
}
// Skip the original Author column
if ($key !== 'author') {
$new_columns[$key] = $title;
}
}
// Fallback if categories or tags were missing
if (!isset($new_columns['contributors'])) {
// Add before date as fallback
$date_pos = array_search('date', array_keys($new_columns), true);
if ($date_pos !== false) {
$new_columns = array_slice($new_columns, 0, $date_pos, true) +
['contributors' => __('Contributors', 'your-text-domain')] +
array_slice($new_columns, $date_pos, null, true);
} else {
$new_columns['contributors'] = __('Contributors', 'your-text-domain'); // Add at end
}
}
if (!isset($new_columns['word_count'])) {
// Add before date as fallback
$date_pos = array_search('date', array_keys($new_columns), true);
if ($date_pos !== false) {
$new_columns = array_slice($new_columns, 0, $date_pos, true) +
['word_count' => __('Word Count', 'your-text-domain')] +
array_slice($new_columns, $date_pos, null, true);
} else {
$new_columns['word_count'] = __('Word Count', 'your-text-domain'); // Add at end
}
}
return $new_columns;
}
add_filter('manage_posts_posts_columns', 'my_plugin_modify_post_table_headers');
// --- Column Content ---
/**
* Display content for custom columns in the Posts list table.
* Handles 'word_count' and 'contributors'.
*
* @param string $column_name The name of the column.
* @param int $post_id The ID of the current post.
*/
function my_plugin_display_custom_post_table_content(string $column_name, int $post_id): void
{
switch ($column_name) {
case 'word_count':
$post = get_post($post_id);
if ($post) {
$word_count = str_word_count(strip_tags($post->post_content));
echo esc_html($word_count);
} else {
echo '—';
}
break;
case 'contributors':
$author_ids = [];
$post = get_post($post_id);
if ($post && isset($post->post_author)) {
$author_ids[] = (int) $post->post_author;
}
$revisions = wp_get_post_revisions($post_id, ['fields' => 'post_author']);
if (!empty($revisions)) {
foreach ($revisions as $revision) {
if (isset($revision->post_author)) {
$author_ids[] = (int) $revision->post_author;
}
}
}
$unique_author_ids = array_unique($author_ids);
$author_links = [];
if (!empty($unique_author_ids)) {
foreach ($unique_author_ids as $author_id) {
$user_info = get_userdata($author_id);
if ($user_info) {
$display_name = $user_info->display_name;
if (current_user_can('edit_users')) {
$link = get_edit_user_link($author_id);
$author_links[] = sprintf('<a href="%s">%s</a>', esc_url($link), esc_html($display_name));
} else {
$author_links[] = esc_html($display_name);
}
}
}
}
echo !empty($author_links) ? implode(', ', $author_links) : '—';
break;
}
}
add_action('manage_posts_posts_custom_column', 'my_plugin_display_custom_post_table_content', 10, 2);
?>
Custom Columns for Custom Post Types
Yes! You can apply this same functionality to any custom post type (CPT). Simply replace posts
in the hook names with your CPT slug.
For a CPT named your_cpt
, the hooks would be:
manage_your_cpt_posts_columns
(for headers)manage_your_cpt_posts_custom_column
(for content)
Use the same callback functions (or create separate ones if the logic needs to differ significantly for the CPT), adjusting the filter/action tags accordingly.
<?php
$post_type = 'your_cpt'; // Replace with your CPT slug
// Example for CPT headers
add_filter("manage_{$post_type}_posts_columns", 'my_plugin_modify_post_table_headers');
// Example for CPT content
add_action("manage_{$post_type}_posts_custom_column", 'my_plugin_display_custom_post_table_content', 10, 2);
?>
And voila! Custom column content fun for all the family (and all the post types).