Add Custom Order Status Dropdown in WooCommerce Admin
Learn how to add an internal custom order dropdown
to WooCommerce orders, so you can save and display an additional
status such as Dispatched or Not dispatched.
What this snippet does
By default, WooCommerce includes core order statuses such as Pending, Processing, Completed and Cancelled. In many stores, however, you may need an extra internal indicator without creating a full custom WooCommerce order status.
This snippet adds a custom dropdown inside the order edit screen, saves the selected value as order meta and displays it in the WooCommerce orders list.
1) Why add a custom order dropdown?
An internal order status dropdown is useful when you want to store additional information for each order without mixing it with the default WooCommerce order statuses.
- Track internal states such as “Dispatched”, “Packed” or “Awaiting Courier”.
- Improve order management inside WooCommerce Admin.
- Give staff a clear status update without changing the main order status flow.
- Display a quick visual indicator directly inside the WooCommerce orders list.
2) Before adding the code
Before using any snippet that modifies the WooCommerce admin area, make sure you follow a basic safety checklist.
- Take a full backup of your files and database.
- Test the snippet on a staging environment first.
- Use a child theme or a small custom plugin, not the parent theme directly.
- Confirm that admin users can save orders correctly after adding the snippet.
- If you use HPOS, test the WooCommerce orders list after installation.
3) Code Snippet
Add the following code to your child theme’s functions.php
file or, preferably, inside a small custom plugin.
/**
* Add a custom internal order status dropdown in WooCommerce admin.
* Stores the value as order meta and displays it in the orders list.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Available internal status options.
*/
function vgdev_internal_order_status_options() {
return array(
'not_dispatched' => __( 'Not dispatched', 'woocommerce' ),
'dispatched' => __( 'Dispatched', 'woocommerce' ),
'packed' => __( 'Packed', 'woocommerce' ),
'awaiting_courier' => __( 'Awaiting courier', 'woocommerce' ),
);
}
/**
* Get saved internal status for an order.
*/
function vgdev_get_internal_order_status( $order ) {
if ( ! $order instanceof WC_Order ) {
return 'not_dispatched';
}
$status = $order->get_meta( '_vgdev_internal_order_status', true );
$options = vgdev_internal_order_status_options();
return array_key_exists( $status, $options ) ? $status : 'not_dispatched';
}
/**
* Add dropdown inside the order edit screen.
*/
add_action( 'woocommerce_admin_order_data_after_order_details', 'vgdev_add_internal_order_status_dropdown' );
function vgdev_add_internal_order_status_dropdown( $order ) {
if ( ! $order instanceof WC_Order ) {
return;
}
$saved_status = vgdev_get_internal_order_status( $order );
$options = vgdev_internal_order_status_options();
wp_nonce_field( 'vgdev_save_internal_order_status', 'vgdev_internal_order_status_nonce' );
?>
<p class="form-field form-field-wide">
<label for="vgdev_internal_order_status">
<?php esc_html_e( 'Internal order status', 'woocommerce' ); ?>
</label>
<select id="vgdev_internal_order_status" name="vgdev_internal_order_status">
<?php foreach ( $options as $value => $label ) : ?>
<option value="<?php echo esc_attr( $value ); ?>" <?php selected( $saved_status, $value ); ?>>
<?php echo esc_html( $label ); ?>
</option>
<?php endforeach; ?>
</select>
<span class="description">
<?php esc_html_e( 'Used internally inside WooCommerce admin only.', 'woocommerce' ); ?>
</span>
</p>
<?php
}
/**
* Save dropdown value when the order is saved.
*/
add_action( 'woocommerce_process_shop_order_meta', 'vgdev_save_internal_order_status_dropdown', 20, 1 );
function vgdev_save_internal_order_status_dropdown( $order_id ) {
if (
! isset( $_POST['vgdev_internal_order_status_nonce'] ) ||
! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['vgdev_internal_order_status_nonce'] ) ), 'vgdev_save_internal_order_status' )
) {
return;
}
if ( ! current_user_can( 'edit_shop_order', $order_id ) ) {
return;
}
$order = wc_get_order( $order_id );
if ( ! $order ) {
return;
}
$options = vgdev_internal_order_status_options();
$status = isset( $_POST['vgdev_internal_order_status'] )
? sanitize_key( wp_unslash( $_POST['vgdev_internal_order_status'] ) )
: 'not_dispatched';
if ( ! array_key_exists( $status, $options ) ) {
$status = 'not_dispatched';
}
$order->update_meta_data( '_vgdev_internal_order_status', $status );
$order->save();
}
/**
* Add column to legacy WooCommerce orders list.
*/
add_filter( 'manage_edit-shop_order_columns', 'vgdev_add_internal_order_status_column', 20 );
/**
* Add column to HPOS WooCommerce orders list.
*/
add_filter( 'manage_woocommerce_page_wc-orders_columns', 'vgdev_add_internal_order_status_column', 20 );
function vgdev_add_internal_order_status_column( $columns ) {
$new_columns = array();
foreach ( $columns as $key => $column ) {
$new_columns[ $key ] = $column;
if ( 'order_status' === $key ) {
$new_columns['vgdev_internal_order_status'] = __( 'Internal status', 'woocommerce' );
}
}
return $new_columns;
}
/**
* Render column for legacy orders list.
*/
add_action( 'manage_shop_order_posts_custom_column', 'vgdev_render_internal_order_status_column_legacy', 20, 2 );
function vgdev_render_internal_order_status_column_legacy( $column, $post_id ) {
if ( 'vgdev_internal_order_status' !== $column ) {
return;
}
$order = wc_get_order( $post_id );
vgdev_render_internal_order_status_badge( $order );
}
/**
* Render column for HPOS orders list.
*/
add_action( 'manage_woocommerce_page_wc-orders_custom_column', 'vgdev_render_internal_order_status_column_hpos', 20, 2 );
function vgdev_render_internal_order_status_column_hpos( $column, $order ) {
if ( 'vgdev_internal_order_status' !== $column ) {
return;
}
vgdev_render_internal_order_status_badge( $order );
}
/**
* Output admin badge.
*/
function vgdev_render_internal_order_status_badge( $order ) {
if ( ! $order instanceof WC_Order ) {
echo '—';
return;
}
$status = vgdev_get_internal_order_status( $order );
$options = vgdev_internal_order_status_options();
$label = isset( $options[ $status ] ) ? $options[ $status ] : $options['not_dispatched'];
echo '<mark class="order-status status-' . esc_attr( $status ) . '"><span>' . esc_html( $label ) . '</span></mark>';
}
4) How it works
vgdev_internal_order_status_options() function defines the available dropdown options.
woocommerce_admin_order_data_after_order_details hook adds the field to the order edit screen.
_vgdev_internal_order_status.
shop_order and wc-orders.
5) How to change the dropdown options
The available options are defined inside vgdev_internal_order_status_options().
You can add, remove or rename statuses based on your store workflow.
function vgdev_internal_order_status_options() {
return array(
'not_dispatched' => __( 'Not dispatched', 'woocommerce' ),
'dispatched' => __( 'Dispatched', 'woocommerce' ),
'packed' => __( 'Packed', 'woocommerce' ),
'awaiting_courier' => __( 'Awaiting courier', 'woocommerce' ),
);
}
6) Where to add the code
There are three common ways to add this snippet to your WooCommerce site.
- Child theme functions.php: a quick solution, as long as you are using a child theme.
- Custom plugin: the cleaner and safer option, because the functionality does not depend on the active theme.
- Snippet plugin: practical if you already manage snippets from the WordPress admin.
7) Testing after installation
After adding the snippet, run a quick test to confirm that everything works correctly.
- Open an existing WooCommerce order.
- Find the Internal order status field.
- Select a value, for example Dispatched.
- Save or update the order.
- Return to the WooCommerce orders list.
- Confirm that the new column displays the correct value.
8) Best practices
- Use simple labels that your team can understand immediately.
- Avoid adding too many dropdown options.
- Keep this field for internal order management, not customer-facing updates.
- Test the snippet on staging before adding it to the live site.
- On multilingual sites, translate the labels for each language.
- If you use HPOS, test the column inside the new WooCommerce orders list.
9) Frequently Asked Questions
No. The snippet saves a separate order meta value and does not change the core WooCommerce order status.
No by default. The information is displayed only inside WooCommerce admin.
Yes. Edit the options inside the vgdev_internal_order_status_options() function.
The snippet uses WooCommerce order meta methods and includes hooks for the new HPOS orders list. Still, you should always test it on your own setup.
The value is stored in the order meta key _vgdev_internal_order_status.
10) Support
For any issue or question related to WooCommerce snippets, you can contact technical support.