Building a Custom WordPress Admin Page with WP_List_Table

When developing custom WordPress functionality, displaying structured data in the administration area often becomes necessary. While WordPress uses WP_List_Table internally to display posts, pages, and users, developers can also extend this class to build custom admin screens with pagination, search, and action handlers.

In this tutorial, we will walk through creating a custom admin page that manages user approval processes using WP_List_Table and WP_User_Query.

1. Registering the Custom Admin Menu

First, we need to register a menu item in the WordPress admin sidebar. We will also include a dynamic badge showing the count of pending accounts waiting for approval.

				
					if (!defined('ABSPATH')) {
    exit;
}

if (!class_exists('WP_List_Table')) {
    require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
}

add_action('admin_menu', 'wg_register_approve_researchers_menu');

function wg_register_approve_researchers_menu() {
    $count = count(get_users([
        'role' => 'researcher',
        'meta_key' => 'status_account',
        'meta_value' => 'Reviewing',
        'fields' => 'ID'
    ]));

    $title = 'Approve Researchers';
    if ($count) {
        $title .= ' <span class="awaiting-mod">' . $count . '</span>';
    }

    add_menu_page(
        'Approve Researchers',
        $title,
        'list_users',
        'approve-researchers',
        'wg_approve_researchers_page',
        'dashicons-groups',
        25
    );
}
				
			

Key points in this setup:

  • class-wp-list-table.php: Since WP_List_Table is an internal core class, it must be explicitly loaded if not already available.
  • Dynamic Badge: Querying pending users allows us to append to the menu title for native notification styling

2. Creating the Custom List Table Class

To display user records cleanly, we extend WP_List_Table. We define custom columns, sortable fields, and the prepare_items() method to query users with pagination and search functionality.

				
					class WG_Approve_Researchers_Table extends WP_List_Table {

    public function get_columns() {
        return [
            'display_name' => 'Name',
            'user_email'   => 'Email',
            'status'       => 'Status'
        ];
    }

    public function get_sortable_columns() {
        return [
            'display_name' => ['display_name', true],
            'user_email'   => ['user_email', false]
        ];
    }

    public function prepare_items() {
        $per_page     = 20;
        $current_page = $this->get_pagenum();
        $search       = !empty($_REQUEST['s']) ? sanitize_text_field($_REQUEST['s']) : '';

        $args = [
            'role'           => 'researcher',
            'number'         => $per_page,
            'paged'          => $current_page,
            'search'         => $search ? '*' . $search . '*' : '',
            'search_columns' => ['user_login', 'user_email', 'display_name'],
            'meta_key'       => 'status_account',
            'meta_value'     => 'Reviewing'
        ];

        if (!empty($_GET['orderby'])) {
            $args['orderby'] = sanitize_key($_GET['orderby']);
        }

        if (!empty($_GET['order'])) {
            $args['order'] = sanitize_key($_GET['order']);
        }

        $query       = new WP_User_Query($args);
        $this->items = $query->get_results();

        $this->set_pagination_args([
            'total_items' => $query->get_total(),
            'per_page'    => $per_page,
            'total_pages' => ceil($query->get_total() / $per_page)
        ]);

        $this->_column_headers = [
            $this->get_columns(),
            [],
            $this->get_sortable_columns()
        ];
    }

    public function column_display_name($item) {
        $url = admin_url('admin.php?page=approve-researchers&user_id=' . $item->ID);

        $actions = [
            'view' => '<a href="' . esc_url($url) . '">View</a>'
        ];

        return sprintf(
            '<strong><a href="%s">%s</a></strong>%s',
            esc_url($url),
            esc_html($item->display_name),
            $this->row_actions($actions)
        );
    }

    public function column_user_email($item) {
        return esc_html($item->user_email);
    }

    public function column_status($item) {
        return esc_html(get_user_meta($item->ID, 'status_account', true));
    }

    public function column_default($item, $column_name) {
        switch ($column_name) {
            case 'user_email':
                return $this->column_user_email($item);
            case 'status':
                return $this->column_status($item);
        }
        return '';
    }
}
				
			

3. Handling User Details and Approval Actions

Finally, we implement the page callback function wg_approve_researchers_page(). It processes the approval POST request, sends a transactional notification email with login credentials, and renders either the table view or individual record details.

				
					function wg_approve_researchers_page() {

    if (isset($_POST['approve_user'])) {
        check_admin_referer('wg_approve_user');

        $user_id     = absint($_POST['user_id']);
        $paymentLink = $_POST['payment_link'];

        if ($user_id) {
            update_user_meta($user_id, 'status_account', 'Active');
            update_user_meta($user_id, 'payment_statust', 'Unpaid');

            $user_data = get_userdata($user_id);

            if ($user_data) {
                $new_password = wp_generate_password(12, true, false);
                wp_set_password($new_password, $user_id);

                $site_name = get_bloginfo('name');
                $site_url  = home_url();
                $email     = $user_data->user_email;
                $full_name = $user_data->display_name;

                $to      = $email;
                $subject = 'Account Approved - ' . $site_name;

                $body = "
                <p>Hi " . esc_html($full_name) . ",</p>
                <p>Great news! Your account on <strong>" . esc_html($site_name) . "</strong> has been approved and successfully activated by our team.</p>
                <p>Next, you'll need to pay for access to our service. Once we've verified your payment, you'll receive your account login credentials.</p>
                <p><strong>Your Payment Details:</strong><br>
                Link: <a href='" . esc_url($paymentLink) . "' style='color: #0073aa; text-decoration: underline;'>Pay Here</a></p>
                <p>Regards,<br>All at " . esc_html($site_name) . "</p>";

                $headers = array('Content-Type: text/html; charset=UTF-8');
                wp_mail($to, $subject, $body, $headers);
            }
        }

        wp_safe_redirect(admin_url('admin.php?page=approve-researchers&user_id=' . $user_id));
        exit;
    }

    $user = false;

    if (!empty($_GET['user_id'])) {
        $user = get_user_by('ID', absint($_GET['user_id']));
    }

    if (!$user) {
        $table = new WG_Approve_Researchers_Table();
        $table->prepare_items();
        ?>
        <div class="wrap">
            <h1 class="wp-heading-inline">Approve Researchers</h1>
            <form method="get">
                <input type="hidden" name="page" value="approve-researchers">
                <?php
                $table->search_box('Search', 'researchers');
                $table->display();
                ?>
            </form>
        </div>
        <?php
        return;
    }

    $status = get_user_meta($user->ID, 'status_account', true);
    ?>

    <div class="wrap">
        <h1 class="wp-heading-inline"><?php echo esc_html($user->display_name); ?></h1>
        <p>
            <a href="<?php echo esc_url(admin_url('admin.php?page=approve-researchers')); ?>">
                ← Back to list
            </a>
        </p>

        <table class="form-table">
            <tr>
                <th>Name</th>
                <td><?php echo esc_html($user->display_name); ?></td>
            </tr>
            <tr>
                <th>Email</th>
                <td><?php echo esc_html($user->user_email); ?></td>
            </tr>
            <tr>
                <th>Status</th>
                <td><?php echo esc_html($status ?: '-'); ?></td>
            </tr>
        </table>

        <?php if ($status !== 'Active'): ?>
            <form method="post">
                <?php wp_nonce_field('wg_approve_user'); ?>
                <input type="hidden" name="user_id" value="<?php echo absint($user->ID); ?>">

                <label for="payment_link"><strong>Payment Link</strong></label><br>
                <input type="text" name="payment_link" id="payment_link" required>

                <p>
                    <input type="submit" name="approve_user" class="button button-primary" value="Approve">
                </p>
            </form>
        <?php endif; ?>
    </div>
    <?php
}
				
			

Conclusion & CTA

Extending WP_List_Table gives your custom WordPress administration features a clean, native UI while supporting standard workflows like sorting, pagination, and searching out of the box.

Be sure to check out our previous tutorials on WordPress development:

Building Custom Gutenberg Blocks Without Webpack: A Practical Dynamic Block Example
jQuery.ajax vs Vanilla JavaScript: Fetch and XHR Compared

If you have any questions, feel free to contact us. We’re here to help! Drop us a message with your project details, and our team will get back to you as soon as possible.

Comments

No comments yet

Share Your Thoughts

Join the conversation!

Scroll to Top

Share Your Thoughts

Thank you for your comment!

Your email address will not be published. Required fields are marked *

Pro

Thank you for reaching out! We’ll be in touch within 24 hours to discuss your project

Standard

Thank you for reaching out! We’ll be in touch within 24 hours to discuss your project

Basic

Thank you for reaching out! We’ll be in touch within 24 hours to discuss your project