How to Drag and Drop Reorder WordPress Pages, Posts, and Products

Table of Contents

Reordering content in WordPress can be surprisingly awkward if you rely on default dates, titles, or manual menu settings. Specifically, if you want to change the sequence of pages, posts, or products quickly, a drag-and-drop approach is the most intuitive solution.

By using a code snippet, you can enable a visual sorting interface directly within your admin area. This allows you to architect your store and site layouts with surgical precision, ensuring the most important content always appears first. 🏎️💨

What Drag and Drop Ordering Actually Does 🏛️

Manual sorting allows you to set the sequence of items in the WordPress admin area by clicking, holding, and moving them into place. Instead of being restricted by rigid default sorting rules, you gain total editorial control.

Common default rules often include:

  • Publish Date: Great for news, but bad for featured products.
  • Alphabetical Order: Useful for directories, but rarely for marketing.
  • ID Order: Purely technical and often irrelevant to the user experience.

Where You Can Apply This Logic 🤝

This “New Way” of manual sorting is versatile. Furthermore, it can be applied across almost every WordPress content type, including:

  • Pages & Posts: For curated reading lists.
  • WooCommerce Products: To highlight bestsellers or seasonal items.
  • Custom Post Types: Perfect for portfolios, team members, or testimonials.

The Setup: Enabling Visual Sorting 🛠️

1. Activate Your Snippet

To get started, add the reordering snippet to your Code Snippets Pro dashboard. Because this feature is handled via a snippet, you avoid the overhead of a dedicated “reorder plugin.” Consequently, your site remains lean while gaining professional-grade functionality.

2. Toggle Reorder Mode

Once the snippet is active, a new “Reorder” option will appear in your admin list screens. Specifically, this switches the view into a sortable mode, allowing you to move items freely.

3. Align Your Front-End Queries 🔐

This is the most critical architectural step. Rearranging items in the admin area only helps if your website is set to respect that manual order. Therefore, you must ensure your loop grids, archives, or widgets are configured to sort by “Menu Order” or “Manual Order” rather than date or title.

Why This Approach is the “New Way” 🚀

Using a code snippet for reordering is popular among developers because it is:

  • Fast: Move dozens of items in seconds.
  • Visual: See the exact sequence as you arrange it.
  • Persistent: Your chosen order remains saved even after you log out.
  • Bloat-Free: No extra plugin files or unnecessary database tables.

Common Mistakes to Avoid 🚫

  1. Ignoring the Query: Many users assume the front end will update automatically. Specifically, you must verify your display settings to ensure they follow the new order.
  2. Wrong Content Type: Remember that product order does not affect page order. You must enable the logic for each specific post type.
  3. Widget Overrides: Some page builders use their own internal sorting logic. Consequently, you may need to adjust those specific widget settings to “Manual.”

Final Takeaway 💎

If you want to curate your WordPress site with surgical precision, drag-and-drop sorting is the answer. It provides the visual control needed to highlight your best work and products instantly. Architect your site your way—simple, fast, and bloat-free.

Ready to reorder? Save your snippet and start curating your content today. 🚀

				
					/**
 * Universal Admin Reorder (PATCHED)
 * Fixes:
 * - DO NOT force posts_per_page = -1 (keeps pagination fast)
 * - DO NOT interfere with admin-ajax (Quick Edit / inline-save)
 * - Save Quick Edit menu_order WITHOUT wp_update_post recursion
 * - Minor hardening + cleanup for columns/bulk actions across supported post types
 */

/**
 * 1) Enable menu_order support for all public post types including WooCommerce Products
 */
add_action('init', function () {
    $post_types = get_post_types(['public' => true], 'names');

    foreach ($post_types as $pt) {
        // Add "page-attributes" support (menu_order lives here in WP UI)
        if (!post_type_supports($pt, 'page-attributes')) {
            add_post_type_support($pt, 'page-attributes');
        }

        // Ensure products also have it (some setups omit it)
        if ($pt === 'product' && !post_type_supports($pt, 'page-attributes')) {
            add_post_type_support($pt, 'page-attributes');
        }
    }
}, 20);


/**
 * 2) Default admin post order by menu_order when supported and no explicit sort selected
 * PATCH: do NOT set posts_per_page = -1, and do NOT run during AJAX (Quick Edit)
 */
add_action('pre_get_posts', function ($q) {
    if (!is_admin() || !$q->is_main_query()) return;

    // Critical: do not touch admin-ajax requests (Quick Edit / inline-save etc.)
    if (wp_doing_ajax()) return;

    // Only on list table screens (edit.php)
    if (!function_exists('get_current_screen')) return;
    $screen = get_current_screen();
    if (!$screen || $screen->base !== 'edit') return;

    $post_type = $q->get('post_type');
    if (!$post_type) return;

    // Some admin screens provide array post_type – normalize
    if (is_array($post_type)) {
        $post_type = reset($post_type);
    }

    if (!post_type_supports($post_type, 'page-attributes')) return;

    // If user chose an orderby that isn't menu_order, respect it
    $orderby = isset($_GET['orderby']) ? sanitize_key($_GET['orderby']) : '';
    if ($orderby && $orderby !== 'menu_order') return;

    $q->set('orderby', 'menu_order');
    $q->set('order', 'ASC');

    // PATCH: keep pagination (no posts_per_page = -1)
}, 20);


/**
 * 3) Enqueue jQuery UI Sortable
 */
add_action('admin_enqueue_scripts', function () {
    wp_enqueue_script('jquery-ui-sortable');
});


/**
 * 4) Add reorder toggle button and script (edit.php only)
 */
add_action('admin_footer-edit.php', function () {
    if (!function_exists('get_current_screen')) return;
    $screen = get_current_screen();
    if (!$screen || empty($screen->post_type)) return;
    if (!post_type_supports($screen->post_type, 'page-attributes')) return;

    $nonce = wp_create_nonce('universal_order_nonce');
    $post_type = esc_js($screen->post_type);
    ?>
    <style>
    .reorder-button{display:inline-block;margin:1px 0 0 8px;padding:0 10px;line-height:2.15384615;font-size:13px;vertical-align:top;text-decoration:none;border:1px solid #2271b1;border-radius:3px;background:#f6f7f7;color:#2271b1;cursor:pointer;transition:all .2s ease}
    .reorder-button:hover{background:#f0f0f1;border-color:#0a4b78;color:#0a4b78}
    .reorder-button::before{content:'';margin-right:5px;font-weight:700;display:inline-block;transition:transform .3s ease}
    .reorder-button.active{background:#2271b1!important;border-color:#2271b1!important;color:#fff!important}
    .reorder-button.active:hover{background:#135e96!important;border-color:#135e96!important}
    .reorder-button.active::before{transform:rotate(90deg)}
    table.wp-list-table .column-reorder_handle{display:none}
    body.reorder-mode table.wp-list-table .column-reorder_handle{display:table-cell;width:40px;text-align:center}
    .drag-handle{display:none;width:24px;height:24px;border-radius:4px;background:#f0f0f1;border:1px solid #c3c4c7;position:relative;transition:all .2s ease;margin:0 auto}
    .drag-handle::before{content:'';position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:12px;height:2px;background:#787c82;box-shadow:0 4px 0 #787c82,0 -4px 0 #787c82;border-radius:1px}
    body.reorder-mode .drag-handle{display:block;cursor:move;animation:fadeIn .3s ease-out}
    @keyframes fadeIn{from{opacity:0;transform:scale(.8)}to{opacity:1;transform:scale(1)}}
    body.reorder-mode .drag-handle:hover{background:#2271b1;border-color:#2271b1;transform:scale(1.1)}
    body.reorder-mode .drag-handle:hover::before{background:#fff;box-shadow:0 4px 0 #fff,0 -4px 0 #fff}
    body.reorder-mode tr.ui-sortable-helper{display:table!important;table-layout:fixed!important;width:auto;background:#fff;box-shadow:0 3px 6px rgba(0,0,0,.15);opacity:.9}
    body.reorder-mode tr.ui-sortable-placeholder{background:#f0f0f1;visibility:visible!important;position:relative}
    body.reorder-mode tr.ui-sortable-placeholder td{background:rgba(34,113,177,.05)}
    .reorder-notice{display:none;background:#f0f7ff;border-left:4px solid #2271b1;padding:12px;margin:10px 0;animation:slideDown .3s ease-out}
    body.reorder-mode .reorder-notice{display:block}
    @keyframes slideDown{from{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}
    .reorder-saving{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:rgba(0,0,0,.8);color:#fff;padding:20px 40px;border-radius:4px;z-index:100000;display:none}
    .reorder-saving.active{display:block}
    body.reorder-mode .row-actions{visibility:hidden}
    body.reorder-mode tr:hover{background:#f6f7f7}
    .column-menu_order{width:60px;text-align:center}
    .order-badge{display:inline-block;background:#dcdcde;color:#50575e;padding:2px 8px;border-radius:3px;font-size:12px;font-weight:500}
    body.reorder-mode .order-badge{background:#2271b1;color:#fff}
    </style>

    <div class="reorder-notice">
        <strong>Reorder Mode Active:</strong> Drag the handles to reorder items. Changes save automatically. Click "Toggle Reorder" again when done.
    </div>
    <div class="reorder-saving">
        <span class="spinner is-active" style="float:left;margin:0 10px 0 0;"></span>
        Saving order...
    </div>

    <script>
    jQuery(function($) {
        var $button = $('<a href="#" class="reorder-button">Toggle Reorder</a>');

        setTimeout(function() {
            var $topNav = $('.tablenav.top');
            if ($topNav.length) {
                var $actionsArea = $topNav.find('.alignleft.actions.bulkactions');
                if ($actionsArea.length) {
                    var $lastAction = $actionsArea.siblings('.alignleft.actions').last();
                    if ($lastAction.length) $lastAction.append($button);
                    else $actionsArea.append($button);
                } else {
                    $topNav.find('.alignleft').last().append($button);
                }
            }
        }, 100);

        var $titleArea = $('.wrap h1, .wrap h2').first();
        $('.reorder-notice').insertAfter($titleArea);

        var reorderActive = false;
        var $tbody = $('table.wp-list-table tbody');
        var originalTitle = document.title;

        $(document).on('keydown', function(e) {
            if ((e.ctrlKey || e.metaKey) && e.key === 'r' && !$(e.target).is(':input')) {
                e.preventDefault();
                $button.trigger('click');
            }
        });

        $button.on('click', function (e) {
            e.preventDefault();

            reorderActive = !reorderActive;
            $('body').toggleClass('reorder-mode', reorderActive);
            $button.toggleClass('active', reorderActive);
            document.title = reorderActive ? ' Reordering - ' + originalTitle : originalTitle;

            if (reorderActive) {
                $tbody.sortable({
                    items: 'tr',
                    axis: 'y',
                    handle: 'td.column-reorder_handle',
                    tolerance: 'pointer',
                    helper: function(e, tr) {
                        var $originals = tr.children();
                        var $helper = tr.clone();
                        $helper.children().each(function(index) {
                            $(this).width($originals.eq(index).width());
                        });
                        return $helper;
                    },
                    start: function(e, ui) {
                        ui.placeholder.height(ui.item.height());
                    },
                    update: function(e, ui) {
                        var movedId = (ui.item.attr('id') || '').replace('post-', '');
                        var newIndex = ui.item.index();

                        $('.reorder-saving').addClass('active');

                        $.post(ajaxurl, {
                            action: 'save_post_order',
                            post_id: movedId,
                            new_index: newIndex,
                            post_type: '<?php echo $post_type; ?>',
                            security: '<?php echo $nonce; ?>'
                        }, function(res) {
                            $('.reorder-saving').removeClass('active');

                            if (res && res.success) {
                                $tbody.find('tr').each(function(index) {
                                    $(this).find('td.column-menu_order .order-badge').text(index + 1);
                                });
                            } else {
                                alert((res && res.data) ? res.data : 'Order failed');
                                $tbody.sortable('cancel');
                            }
                        }).fail(function() {
                            $('.reorder-saving').removeClass('active');
                            alert('Network error. Please try again.');
                            $tbody.sortable('cancel');
                        });
                    }
                });
            } else {
                if ($tbody.data('ui-sortable')) $tbody.sortable('destroy');
            }
        });

        $(window).on('beforeunload', function() {
            if (reorderActive) $('body').removeClass('reorder-mode');
        });
    });
    </script>
    <?php
});


/**
 * 5) Handle AJAX reorder saving
 * NOTE: This still reorders the whole list and updates menu_order for each item.
 * That’s expected (and only runs when you drag/drop), but it’s NOT part of Quick Edit.
 */
add_action('wp_ajax_save_post_order', function () {
    if (!current_user_can('edit_others_posts')) {
        wp_send_json_error('Permission denied');
    }
    if (!check_ajax_referer('universal_order_nonce', 'security', false)) {
        wp_send_json_error('Bad nonce');
    }

    $post_id   = isset($_POST['post_id']) ? (int) $_POST['post_id'] : 0;
    $new_index = isset($_POST['new_index']) ? (int) $_POST['new_index'] : 0;
    $post_type = sanitize_key($_POST['post_type'] ?? '');

    if (!$post_id || !$post_type) {
        wp_send_json_error('Invalid data');
    }

    $args = [
        'post_type'      => $post_type,
        'posts_per_page' => -1,
        'post_status'    => ['publish', 'draft', 'pending', 'future', 'private'],
        'orderby'        => 'menu_order',
        'order'          => 'ASC',
        'fields'         => 'ids',
    ];

    $all_ids = get_posts($args);

    if (!in_array($post_id, $all_ids, true)) {
        wp_send_json_error('Post ID not found');
    }

    $from = array_search($post_id, $all_ids, true);
    array_splice($all_ids, $from, 1);
    array_splice($all_ids, $new_index, 0, $post_id);

    // Fast update (avoid triggering save_post hooks repeatedly)
    global $wpdb;
    foreach ($all_ids as $index => $id) {
        $wpdb->update(
            $wpdb->posts,
            ['menu_order' => (int) $index],
            ['ID' => (int) $id],
            ['%d'],
            ['%d']
        );
        clean_post_cache((int) $id);
    }

    wp_send_json_success('Order saved');
});


/**
 * 6) WooCommerce respects menu_order when selected
 */
add_filter('woocommerce_default_catalog_orderby_options', function ($options) {
    $options['menu_order'] = 'Default sorting';
    return $options;
});

add_filter('woocommerce_get_catalog_ordering_args', function ($args) {
    if (isset($_GET['orderby']) && $_GET['orderby'] === 'menu_order') {
        $args['orderby'] = 'menu_order title';
        $args['order']   = 'ASC';
    }
    return $args;
});


/**
 * 7) Add drag handle and "Order" columns to admin tables
 * PATCH: avoid duplicate manage_edit-{$pt}_columns filters by using one filter to add both
 */
add_action('admin_init', function () {
    $post_types = get_post_types(['public' => true], 'names');

    foreach ($post_types as $pt) {
        if (!post_type_supports($pt, 'page-attributes')) continue;

        // Add drag handle + Order column
        add_filter("manage_edit-{$pt}_columns", function ($columns) {
            // Insert handle at start
            $columns = array_merge(['reorder_handle' => ''], $columns);

            // Add order column (near end)
            $columns['menu_order'] = 'Order';
            return $columns;
        });

        // Populate columns
        add_action("manage_{$pt}_posts_custom_column", function ($column, $post_id) {
            if ($column === 'menu_order') {
                $order = (int) get_post_field('menu_order', $post_id);
                echo '<span class="order-badge">' . esc_html($order + 1) . '</span>';
            } elseif ($column === 'reorder_handle') {
                echo '<div class="drag-handle"></div>';
            }
        }, 10, 2);

        // Make Order column sortable
        add_filter("manage_edit-{$pt}_sortable_columns", function ($columns) {
            $columns['menu_order'] = 'menu_order';
            return $columns;
        });
    }
});


/**
 * 8) Add bulk actions for order management (ALL supported post types)
 * PATCH: apply to every page-attributes-supported post type, not just post/page.
 */
add_filter('bulk_actions-edit-post', 'universal_order_add_bulk_actions');
add_filter('bulk_actions-edit-page', 'universal_order_add_bulk_actions');

add_action('admin_init', function () {
    $post_types = get_post_types(['public' => true], 'names');
    foreach ($post_types as $pt) {
        if (in_array($pt, ['post', 'page'], true)) continue;
        if (!post_type_supports($pt, 'page-attributes')) continue;

        add_filter("bulk_actions-edit-{$pt}", 'universal_order_add_bulk_actions');
    }
});

function universal_order_add_bulk_actions($bulk_actions) {
    if (!function_exists('get_current_screen')) return $bulk_actions;
    $screen = get_current_screen();
    if ($screen && !empty($screen->post_type) && post_type_supports($screen->post_type, 'page-attributes')) {
        $bulk_actions['reset_order']   = 'Reset Order';
        $bulk_actions['reverse_order'] = 'Reverse Order';
    }
    return $bulk_actions;
}


/**
 * 9) Handle bulk actions (ALL supported post types)
 */
add_filter('handle_bulk_actions-edit-post', 'universal_order_handle_bulk_actions', 10, 3);
add_filter('handle_bulk_actions-edit-page', 'universal_order_handle_bulk_actions', 10, 3);

add_action('admin_init', function () {
    $post_types = get_post_types(['public' => true], 'names');
    foreach ($post_types as $pt) {
        if (in_array($pt, ['post', 'page'], true)) continue;
        if (!post_type_supports($pt, 'page-attributes')) continue;

        add_filter("handle_bulk_actions-edit-{$pt}", 'universal_order_handle_bulk_actions', 10, 3);
    }
});

function universal_order_handle_bulk_actions($sendback, $doaction, $post_ids) {
    if ($doaction !== 'reset_order' && $doaction !== 'reverse_order') {
        return $sendback;
    }

    if (!function_exists('get_current_screen')) return $sendback;
    $screen = get_current_screen();
    if (!$screen || empty($screen->post_type)) return $sendback;

    $args = [
        'post_type'      => $screen->post_type,
        'posts_per_page' => -1,
        'post_status'    => ['publish', 'draft', 'pending', 'future', 'private'],
        'orderby'        => $doaction === 'reverse_order' ? 'menu_order' : 'date',
        'order'          => $doaction === 'reverse_order' ? 'DESC' : 'ASC',
        'fields'         => 'ids',
    ];
    $all_ids = get_posts($args);

    global $wpdb;
    foreach ($all_ids as $index => $id) {
        $wpdb->update(
            $wpdb->posts,
            ['menu_order' => (int) $index],
            ['ID' => (int) $id],
            ['%d'],
            ['%d']
        );
        clean_post_cache((int) $id);
    }

    $sendback = add_query_arg('bulk_order_updated', count($all_ids), $sendback);
    return $sendback;
}


/**
 * 10) Display admin notice for bulk actions
 */
add_action('admin_notices', function () {
    if (!empty($_REQUEST['bulk_order_updated']) && !empty($_REQUEST['_wp_http_referer'])) {
        $count = (int) $_REQUEST['bulk_order_updated'];
        echo '<div class="notice notice-success is-dismissible"><p>';
        echo sprintf('Order updated for %d item%s.', $count, $count !== 1 ? 's' : '');
        echo '</p></div>';
        ?>
        <script>
        jQuery(function() {
            if (window.history && window.history.replaceState) {
                var url = window.location.href;
                url = url.replace(/[?&]bulk_order_updated=[^&]+/, '');
                url = url.replace(/&&/, '&').replace(/\?&/, '?');
                if (url.slice(-1) === '?' || url.slice(-1) === '&') url = url.slice(0, -1);
                window.history.replaceState({}, document.title, url);
            }
        });
        </script>
        <?php
    }
});


/**
 * 11) Add quick edit support for menu order
 */
add_action('quick_edit_custom_box', function ($column_name, $post_type) {
    if ($column_name !== 'menu_order') return;
    if (!post_type_supports($post_type, 'page-attributes')) return;
    ?>
    <fieldset class="inline-edit-col-left">
        <div class="inline-edit-col">
            <label>
                <span class="title">Order</span>
                <span class="input-text-wrap">
                    <input type="number" name="menu_order" class="menu_order" value="">
                </span>
            </label>
        </div>
    </fieldset>
    <?php
}, 10, 2);


/**
 * 12) Save quick edit menu order
 * PATCH: do NOT call wp_update_post() in save_post (prevents recursion + slow chains)
 */
add_action('save_post', function ($post_id) {

    if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return;
    if (!current_user_can('edit_post', $post_id)) return;

    // Only handle when field is present
    if (!isset($_POST['menu_order'])) return;

    // Quick Edit includes _inline_edit nonce
    if (isset($_POST['_inline_edit']) && !wp_verify_nonce($_POST['_inline_edit'], 'inlineeditnonce')) {
        return;
    }

    $new_order = (int) $_POST['menu_order'];

    global $wpdb;
    $wpdb->update(
        $wpdb->posts,
        ['menu_order' => $new_order],
        ['ID' => (int) $post_id],
        ['%d'],
        ['%d']
    );
    clean_post_cache((int) $post_id);

}, 10, 1);
				
			

Huge thanks to Imran Siddiq - Web Squadron for architecting this tutorial!

❓FAQ: 

🛠️ Yes. This is a powerful way to ensure your most profitable products appear at the top of your shop grid without changing their publish dates.

💎 Absolutely. Whether it’s a portfolio or a team list, the snippet can be configured to work with any registered post type.

🔍 The most likely reason is that your front-end query is still sorting by another rule (like date). Change the “Order By” setting in your widget or template to “Menu Order.”

❌ No. By using Code Snippets Pro, you can add this functionality with a lightweight snippet, keeping your site fast and clean. 🏎️💨

Picture of Andrea Morgado
Andrea Morgado
She’s a strategist at heart and a compulsory problem-solver who turned “helping brands grow” into a long-term career. As Marketing Lead at Code Snippets, she spends her days shaping stories, building visibility, and translating complex ideas into language humans can understand.. without buzzwords, fluff, or unnecessary SEO filler words. With 15 years of experience across branding, design, web design, and digital marketing, she believes in long-term thinking, clear communication, and doing the work properly before dropping it on LinkedIn. Away from the screen, she’s powered by curiosity, creativity, and a suspiciously reliable ability to spot what’s broken, confusing, or missing, and quietly fix it before anyone else notices.
Share this Post on Social
Facebook
Twitter
LinkedIn
Email

Table of Contents

Crocoblock 8th Birthday Sale

 Celebrate Crocoblock’s 8th Birthday with up to 35% off. Discover how pairing Crocoblock’s dynamic tools with Code Snippets Pro creates the ultimate developer stack

The Code Snippets Pro Affiliate Program

Turn Your Referrals into Revenue.

Love using Code Snippets? Share the “Pro Way” with your audience and earn a 30% lifetime commission on every sale. It’s fast to join, easy to track, and built for your long-term success.