Menu Search
Jump to the content X X
Smashing Conf Barcelona

You know, we use ad-blockers as well. We gotta keep those servers running though. Did you know that we publish useful books and run friendly conferences — crafted for pros like yourself? E.g. our upcoming SmashingConf Barcelona, dedicated to smart front-end techniques and design patterns.

How To Modify Admin Post Lists In WordPress

Have you ever created a custom post type and then found that only the titles and dates of your posts are displayed in the admin lists? While WordPress will add taxonomies for you, that’s the most it can do. Adding relevant at-a-glance information is easy; in this article, we’ll look how to modify admin post lists with WordPress.

To make sure we’re on the same page, an admin list is the table of posts shown in the admin section when you click on “Posts,” “Pages” or another custom post type. Before we delve in, it is worth noting that admin tables are created using the WP_List_Table class. Jeremy Desvaux de Marigny has written a great article on native admin tables1 that explains how to make these from scratch.

Further Reading on SmashingMag: Link

We’ll focus in this article on how to extend existing tables. We’ll do this using an example from a theme that we recently built, named Rock Band5. Rock Band includes event management, which means that we needed some custom event-specific interface elements and details to make the admin section more useful!

Creating A Custom Post Type Link

This process is fairly straightforward and is documented well in “The Complete Guide to Custom Post Types6.” All we need is a definition of the labels that we’re going to use and a few settings. Open up your functions.php file and drop the following into it.

add_action( 'init', 'bs_post_types' );
function bs_post_types() {

  $labels = array(
    'name'                => __( 'Events', THEMENAME ),
    'singular_name'       => __( 'Event', THEMENAME ),
    'add_new'             => __( 'Add New', THEMENAME ),
    'add_new_item'        => __( 'Add New Event', THEMENAME ),
    'edit_item'           => __( 'Edit Event', THEMENAME ),
    'new_item'            => __( 'New Event', THEMENAME ),
    'all_items'           => __( 'All Event', THEMENAME ),
    'view_item'           => __( 'View Event', THEMENAME ),
    'search_items'        => __( 'Search Events', THEMENAME ),
    'not_found'           => __( 'No events found', THEMENAME ),
    'not_found_in_trash'  => __( 'No events found in Trash', THEMENAME ),
    'menu_name'           => __( 'Events', THEMENAME ),

  $supports = array( 'title', 'editor' );

  $slug = get_theme_mod( 'event_permalink' );
  $slug = ( empty( $slug ) ) ? 'event' : $slug;

  $args = array(
    'labels'              => $labels,
    'public'              => true,
    'publicly_queryable'  => true,
    'show_ui'             => true,
    'show_in_menu'        => true,
    'query_var'           => true,
    'rewrite'             => array( 'slug' => $slug ),
    'capability_type'     => 'post',
    'has_archive'         => true,
    'hierarchical'        => false,
    'menu_position'       => null,
    'supports'            => $supports,

  register_post_type( 'event', $args );


Quick Tip Link

By pulling the permalink from the theme settings, you can make sure that users of your theme are able to set their own permalinks. This is important for multilingual websites, on which administrators might want to make sure that URLs are readable by their users.


What we get is the post list above. It’s better than nothing, but it has no at-a-glance information at all. Event venue, start time and ticket status would be great additions, so let’s get cracking!

Adding Custom Table Headers Link

Throughout this whole process, we will never have to touch the WP_Lists_Table class directly. This is wonderful news! Because we’ll be doing everything with hooks, our code will be nice and modular, easily customizable.

Adding the header is as simple as modifying the value of an array. This sounds like a job for a filter!

add_filter('manage_event_posts_columns', 'bs_event_table_head');
function bs_event_table_head( $defaults ) {
    $defaults['event_date']  = 'Event Date';
    $defaults['ticket_status']    = 'Ticket Status';
    $defaults['venue']   = 'Venue';
    $defaults['author'] = 'Added By';
    return $defaults;

Note the name of the filter: It corresponds to the name of the post type we have created. This means you can modify the table of any post type, not only your custom ones. Just use manage_post_posts_columns to modify the columns for the table of regular posts.

Once this code has been placed in our functions file, you should see the four new table headers. The fields don’t have any content yet; it is for us to decide what goes in them.

Fill ’er Up! Link

Adding data for each column is about as “complex” as it was to create the columns.

add_action( 'manage_event_posts_custom_column', 'bs_event_table_content', 10, 2 );

function bs_event_table_content( $column_name, $post_id ) {
    if ($column_name == 'event_date') {
    $event_date = get_post_meta( $post_id, '_bs_meta_event_date', true );
      echo  date( _x( 'F d, Y', 'Event date format', 'textdomain' ), strtotime( $event_date ) );
    if ($column_name == 'ticket_status') {
    $status = get_post_meta( $post_id, '_bs_meta_event_ticket_status', true );
    echo $status;

    if ($column_name == 'venue') {
    echo get_post_meta( $post_id, '_bs_meta_event_venue', true );


As is obvious from the structure of this function, it gets called separately for each column. Because of this, we need to check which column is currently being displayed and then spit out the corresponding data.

The data we need for this is stored in the postmeta table. Here’s how to do it:

  • The event’s date is stored using the _bs_meta_event_date key.
  • The ticket’s status uses the _bs_meta_event_ticket_status key.
  • The venue is stored using the _bs_meta_event_venue meta key.

Because these are all postmeta values, we just need to use the get_post_meta() function to retrieve them. With the exception of the date, we can echo these values right away.

This brings us to an important point. You are not restricted to showing individual snippets of data or showing links. Whatever you output will be shown. With sufficient time, you could attach a calendar to the dates, which would be shown on hover. You could create flyout menus that open up on click, and so on.


As you can see, this is much better. The event’s date, ticket status, venue and author can be seen, which makes this table actually informative, rather than just a way to get to edit pages. However, we can do more.

Ordering Columns Link

Enabling column ordering takes two steps but is fairly straightforward. First, use a filter to specify which of your columns should be sortable by adding it to an array. Then, create a filter for each column to modify the query when a user clicks to sorts the column.

add_filter( 'manage_edit-event_sortable_columns', 'bs_event_table_sorting' );
function bs_event_table_sorting( $columns ) {
  $columns['event_date'] = 'event_date';
  $columns['ticket_status'] = 'ticket_status';
  $columns['venue'] = 'venue';
  return $columns;

add_filter( 'request', 'bs_event_date_column_orderby' );
function bs_event_date_column_orderby( $vars ) {
    if ( isset( $vars['orderby'] ) && 'event_date' == $vars['orderby'] ) {
        $vars = array_merge( $vars, array(
            'meta_key' => '_bs_meta_event_date',
            'orderby' => 'meta_value'
        ) );

    return $vars;

add_filter( 'request', 'bs_ticket_status_column_orderby' );
function bs_ticket_status_column_orderby( $vars ) {
    if ( isset( $vars['orderby'] ) && 'ticket_status' == $vars['orderby'] ) {
        $vars = array_merge( $vars, array(
            'meta_key' => '_bs_meta_event_ticket_status',
            'orderby' => 'meta_value'
        ) );

    return $vars;

add_filter( 'request', 'bs_venue_column_orderby' );
function bs_venue_column_orderby( $vars ) {
    if ( isset( $vars['orderby'] ) && 'venue' == $vars['orderby'] ) {
        $vars = array_merge( $vars, array(
            'meta_key' => '_bs_meta_event_venue',
            'orderby' => 'meta_value'
        ) );

    return $vars;

Here is what’s happening in each of these cases. Whenever posts are listed, an array of arguments is passed that determines what is shown — things like how many to show per page, which post type to display, and so on. WordPress knows how to construct the array of arguments for each of its built-in features.

When we say, “order by venue,” WordPress doesn’t know what this means. Results are ordered before they are displayed, not after the fact. Therefore, WordPress needs to know what order to pull posts in before it actually retrieves them. Thus, we tell WordPress which meta_key to filter by and how to treat the values (meta_value for strings, meta_value_num for integers).

As with displaying data, you can go nuts here. You can use all of the arguments that WP_Query takes to perform taxonomy filtering, meta field queries and so on.

By adding the code above, we can now click to order based on date, status and venue. We’re almost there. One more thing would help out a lot, especially when dealing with hundreds of events.

Data Filtering Link

Setting up the filters is analogous to setting up ordering. First, we tell WordPress which controls we want to use. Then, we need to make sure those controls actually do something. Let’s get started.

add_action( 'restrict_manage_posts', 'bs_event_table_filtering' );
function bs_event_table_filtering() {
  global $wpdb;
  if ( $screen->post_type == 'event' ) {

    $dates = $wpdb->get_results( "SELECT EXTRACT(YEAR FROM meta_value) as year,  EXTRACT( MONTH FROM meta_value ) as month FROM $wpdb->postmeta WHERE meta_key = '_bs_meta_event_date' AND post_id IN ( SELECT ID FROM $wpdb->posts WHERE post_type = 'event' AND post_status != 'trash' ) GROUP BY year, month " ) ;

    echo '';
      echo '' . __( 'Show all event dates', 'textdomain' ) . '';
    foreach( $dates as $date ) {
      $month = ( strlen( $date->month ) == 1 ) ? 0 . $date->month : $date->month;
      $value = $date->year . '-' . $month . '-' . '01 00:00:00';
      $name = date( 'F Y', strtotime( $value ) );

      $selected = ( !empty( $_GET['event_date'] ) AND $_GET['event_date'] == $value ) ? 'selected="select"' : '';
      echo '' . $name . '';
    echo '';

    $ticket_statuses = get_ticket_statuses();
    echo '';
      echo '' . __( 'Show all ticket statuses', 'textdomain' ) . '';
    foreach( $ticket_statuses as $value => $name ) {
      $selected = ( !empty( $_GET['ticket_status'] ) AND $_GET['ticket_status'] == $value ) ? 'selected="selected"' : '';
      echo '' . $name . '';
    echo '';


I know, this is a bit scarier! Initially, all we are doing is making sure that we add filters to the right page. As you can see from the hook, this is not specific to the post’s type, so we need to check manually.

Once we’re sure that we’re on the events page, we add two controls: a selector for event dates and a selector for ticket statuses.

We have one custom function in there, get_ticket_statuses(), which is used to retrieve a list of ticket statuses. These are all defined by the user, so describing how it works would be overkill. Suffice it to say that it contains an array with the key-value pairs that we need for the selector.


Once this is done, the table will reach its final form. We now have our filters along the top, but they don’t work yet. Let’s fix that, shall we?

Filtering data is simply a matter of adding arguments to the query again. This time, instead of ordering our data, we’ll add parameters to narrow down or broaden our returned list of posts.

add_filter( 'parse_query','bs_event_table_filter' );
function bs_event_table_filter( $query ) {
  if( is_admin() AND $query->query['post_type'] == 'event' ) {
    $qv = &$query->query_vars;
    $qv['meta_query'] = array();

    if( !empty( $_GET['event_date'] ) ) {
      $start_time = strtotime( $_GET['event_date'] );
      $end_time = mktime( 0, 0, 0, date( 'n', $start_time ) + 1, date( 'j', $start_time ), date( 'Y', $start_time ) );
      $end_date = date( 'Y-m-d H:i:s', $end_time );
      $qv['meta_query'][] = array(
        'field' => '_bs_meta_event_date',
        'value' => array( $_GET['event_date'], $end_date ),
        'compare' => 'BETWEEN',
        'type' => 'DATETIME'


    if( !empty( $_GET['ticket_status'] ) ) {
      $qv['meta_query'][] = array(
        'field' => '_bs_meta_event_ticket_status',
        'value' => $_GET['ticket_status'],
        'compare' => '=',
        'type' => 'CHAR'

    if( !empty( $_GET['orderby'] ) AND $_GET['orderby'] == 'event_date' ) {
      $qv['orderby'] = 'meta_value';
      $qv['meta_key'] = '_bs_meta_event_date';
      $qv['order'] = strtoupper( $_GET['order'] );


For each filter, we need to add rules to the query. When we’re filtering for events, we need to add a meta_query10. This will return only results for which the custom field key is _bs_meta_event_ticket_status and the value is the given ticket’s status.

Once this final piece of the puzzle is added, we will have a customized WordPress admin list, complete with filtering, ordering and custom data. Well done!

Overview Link

Adding custom data to a table is a great way to draw information to the attention of users. Plugin developers can hook their functionality into posts without touching any other functionality, and theme authors can add advanced information about custom post types and other things to relevant places.

Showing the right information in the right place can make a huge difference in the salability and likability of any product. That being said, don’t overexploit your newfound power. Don’t add fields just because you can, especially to WordPress’ main tables.

Don’t forget that others know about this, too, and many developers of SEO plugins and similar products already add their own columns to posts. If you’re going to add things to the default post types, I suggest including settings to enable and disable them.

If you’ve used these techniques in one of your products or are wondering how to show some tidbit of information in a table, let us know in the comments!

(al, il)

Footnotes Link

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10

↑ Back to top Tweet itShare on Facebook

Hallo, my name is Daniel :) I build plugins, themes and apps - then proceed to write or talk about them. I contribute to various other online sites. When not coding or writing you'll find me playing board games or running with my dog. Drop me a line on Twitter or visit my personal website.

  1. 1

    When creating a custom post type, use the “Post Type Generator” at
    it’s faster and easier.

  2. 3

    You can use this class to create and manage the custom post type columns in a much faster and easier way.

  3. 4

    awesome, I needed this stuff

  4. 5

    Here’s a challenge:

    Imagine you have a CPT that has a date and an AM/PM selector, and for each date there can only be one post for each half of that day. Then imagine if in the backend we want to list these items so that each line has a date, then a link for the AM entry, and a link for the PM entry. Is this possible without heavily rewriting how the List Table class works?

    • 6

      Edit: I’m trying not to strain the edit function, so I deleted this comment and elaborate in the next one.

      • 7

        I struggled a bit with your description at first, but I think I got you. You’re trying to have a line in the post table that contains the “title” columns (i.e. the main column with all the edit links and such) for two posts next to each other.

        I dug a bit into WordPress’ guts and I came to the conclusion that it could be done, but it’s not really elegant and not really easy. The solution would be two-fold: a) filter out the posts that should be combined into another table row, and b) create the table cells for that second post in the first post’s row.

        I can see a way of coercing the table generation to suppress rows, by checking beforehand which posts not to display and supplying that info as a post__not_in parameter to the ‘request’ filter. While you’re at it you could also create a lookup table for the first post to look up which other post to include in the row.

        But the real ugliness comes when the actual table is generated from the post list. Creation of the checkbox and title cells is hardcoded into a switch statement and cannot be called externally, so you’d have to duplicate that part of the code, which is very ugly and not particularly future-proof. Personally, I wouldn’t do it.

        It would be much more straightforward to have the list sorted by a “date” custom field and another for “AM/PM” (sorting by two fields would be a bit icky, however, making it the default sorting then is easy).

        I think the easiest, actually two-klicks-in-a-plugin solution would to sort the table by the date field, and display AM/PM in the column next to it. Sometimes the AM entry would come after PM but I think it’s a reasonable tradeoff.

        You’d still have two lines for two halves of a day, but they would be next to each other and the single entries would carry all the info and modification instruments for the respective posts.

  5. 8

    Bart De Vuyst

    December 5, 2013 1:55 am

    Great article. For those willing to do this with a plugin; here’s a well-made one that does all this. Codepress Admin Columns:

    They have a premium add-on for sorting and filtering too.

    Highly recommended!

    • 9


      I have been creating custom post types and accompanying tables — even sortable and with all sorts of info customizations — by hand pretty much since custom post types became available in WordPress. Back then there were no plugins to do the job in a satisfactory way.

      Then I recently discovered the existence “Codepress Admin Columns”, and I must say it does the job just as if I had the customizations programmed myself. Which I am really picky about.

      The only thing it (naturally) doesn’t do is fill the columns with data that needs to be processed before it can be displayed in a column. But that’s a fringe case really.

  6. 10

    Great one, thank you :)

  7. 11

    Geert van der Heide

    December 5, 2013 2:26 am

    Thanks for this article! It’s a topic I haven’t read much about. Nice to have a quick but complete reference for when I need it.

  8. 12

    “By pulling the permalink from the theme settings, you can make sure that users of your theme are able to set their own permalinks. This is important for multilingual websites, on which administrators might want to make sure that URLs are readable by their users.”

    Just a quick note: permalinks should be also translation-ready – so don’t forget to wrap those into __(); :)


    • 13

      Daniel Pataki

      December 5, 2013 2:52 am

      Hi Tom!

      Good point! I did include a ‘get_theme_mod()’ reference which would allow the user to change it. This would work when creating a site in a different language but not for multilingual site, you’re right :)


  9. 14

    This is very good, one of the most complete guides to managing custom post columns that I’ve seen yet. Will surely use aspects of this for theme development. Thank you Daniel.

  10. 15

    Very cool! Here a small addon for categories ‘sorry Dutch labels’:
    The only thing what i’am still searching for is how to add a category into the slug?

    Now its build like this;

    $slug = get_theme_mod( ‘event_permalink’ );
    $slug = ( empty( $slug ) ) ? ‘event’ : $slug;

    $args = array(
    ‘rewrite’ => array( ‘slug’ => $slug ),

    Also i did added customfields into the posstype, used this script;

  11. 16

    Nice tutorial, but I’ve got a question though:

    it seems to be from an event calendar. This is what I’m also working on at the moment. I already made one writing my own plugin and now I try to do it using custom post types. All working fine so far, but I’d like to do the following:

    I want to have two lists available for the admin. One with upcoming events and one with past ones. I know how to do it in my own plugin, but how do I go about it when using WPs standard edit page? Can I have there two tables?

    • 17

      Hi Holder,

      Sure, everything is possible :) There are two ways you could tackle this. You can add a simple filter to the top which would allow you to show “all”, “past” or “future”. You can use the code in the data filtering section to set this up.

      If you’d like to add another table to the list the easiest way to do it is to just create your own admin page and add both tables as a custom WP_List_Table. This requires a bit more work but will allow you to be much more flexible with how you show data.

  12. 18

    Hi. Nice tutorial.
    Seems to be a quite popular thing to be able to change the way the post list looks.

    However, what I really would like to have is the possibility to edit custom fields right in the post list. And I am wondering why nobody else seems to need this. There is a plugin that makes it possible to sort posts via drag and drop and I saw one that makes it possible to change some standard post meta data (Admin Management Xtended). But editing custom fields would be really great.

    Sorry if I am wrong. I am quite new to wordpress. I never really got into it because I always had the feeling that it is much too opinionated and unflexible, being mainly a blogging software. But now I know much more is possible than I thought.

  13. 19

    I was walking through the filtering section and ran across an error in your examples above. I think Smashing’s WYSIWYG may be filtering out the HTML in the post, because there aren’t any or tags around the data-filtering lists :-)

    It should look something like this:

    echo '<select name="resource_section">';
    echo '<option value="">' . __( 'Show all sections', 'textdomain' ) . '</option>';
    foreach( $sectionOptions as $value => $name ) {
    $selected = ( !empty( $_GET['resource_section'] ) AND $_GET['resource_section'] == $value ) ? 'selected="selected"' : '';
    echo '<option ' .$selected . ' value="'.$value.'">' . $name . '</option>'; }
    echo '</select>';

    Thanks for the article! It’s been very helpful!

  14. 20

    There was also an issue with the variable $screen in line 4 of Data Filtering: if ( $screen->post_type == 'sdi_resources' ) {.

    To resolve this, I used $current_screen instead:
    global $wpdb, $current_screen;
    if ( $current_screen->post_type == 'sdi_resources' ) {

  15. 21

    I’m having trouble getting the parse_query hook to work. Perhaps the $qv array is not setting the query vars? I’m on 3.6.1 for reference. If anyone could shed some light on this that would be great.

    Otherwise, this tutorial was extremely beneficial. Thanks also to the users who found the bugs and posted them in comments.

  16. 22

    I’ve developed a PHP class to help developers build custom post types quickly.

    You can create post types and taxonomies and customise the admin edit screen with columns and filters. Check it out.

  17. 23

    Anirban Pathak

    March 5, 2014 8:26 pm

    This is very good, one of the most complete guides to managing custom post columns that I’ve seen yet. Will surely use aspects of this for theme development.

  18. 24

    Dave T. Green

    April 9, 2014 3:38 am

    A really nice addition to this (superb) article would be how to actually change the order of the columns. In the example, the published date is the second column…what if I wanted that to be the last column?

    • 25

      Dave T. Green

      April 9, 2014 4:10 am

      To answer my own question, you just unset the columns you want to move, then set them again:

      function property_columns( $defaults ) {
      $defaults[‘featured_image’] = ‘Image’;
      $defaults[‘title’] = ‘Property Name’;
      $defaults[‘reference’] = ‘Ref. No’;
      $defaults[‘price’] = ‘Price’;
      $defaults[‘availability’] = ‘Availability’;
      $defaults[‘date’] = ‘Date’;
      return $defaults;

      • 26

        Another way to do this, if you don’t want to unset and then set again, is to use: array_slice to insert your new columns to the order you want, instead of adding them at the end.

        So for example, if you wanted to add one custom column to your table, let’s say after the ‘title’ it would be like this:

        $column_my_custom_col = array( ‘my_custom_col’ => ‘My Custom Column’ );

        $columns = array_slice( $columns, 0, 2, true ) + $column_my_custom_col + array_slice( $columns, 2, NULL, true );
        return $columns;

        Quicker & easier, right? ;)

  19. 27

    Very helpful but I ran into one issue: the last portion regarding filtering seems to conflict with meta_query when used within get_posts on other plugins in the admin screens (the dashboard for example). Specifically, commenting out the line “$qv = &$query->query_vars;” seems to fix the problems though being somewhat of a novice I’m not sure why this fixes the conflict. The codex is a little weak on query_vars so I don’t understand what causes the conflict.

    Great tutorial though!!!

  20. 28


    June 8, 2014 6:26 am

    If your data is not filtering correctly, i’d suggest changing this line:

    ‘field’ => ‘_bs_meta_event_ticket_status’,

    to this:

    ‘key’ => ‘_bs_meta_event_ticket_status’,

    It worked for me.

  21. 29

    Faysal Haque

    July 17, 2014 8:02 pm

    Thanks for this Great article :)

  22. 30

    Please note, the following must be set: ‘hierarchical’ => false.

    Custom columns will not work if hierarchical set to true on custom post type pages, but will for normal posts.

    Also, the following link is very helpful to figure out how how to do things like add columns to ALL post types’ admin posts tables (eg: custom post types and default post types):–wp-24934

  23. 31

    How to set the width for each column? I have added a column in the list of posts but now I need to resize it. Thank you.

  24. 32

    Nice Bro,

    You Rocks….

  25. 33

    Cool stuff. Can you provide some more details on the get_roundtable_children() function? I’d like to include an admin column in one cpt populated by an acf field from another cpt and trying to figure out how to do this.
    – Thanks


↑ Back to top