Every WordPress site sends emails — password resets, new user notifications, comment moderation alerts, WooCommerce order confirmations. By default all of them arrive as plain text with a generic subject line and no branding. The wp_mail() function is straightforward to use directly, and the wp_mail_content_type filter switches the entire mail engine to HTML in one line. The harder part is building a reusable HTML email template that renders consistently across email clients — Outlook’s rendering engine in particular ignores most modern CSS. The safe approach is table-based layout with inline styles, a practice that feels dated but remains the only reliable cross-client method in 2020. This article shows a complete pattern: a PHP template file that accepts variables, a helper function that loads it and calls wp_mail(), and the filters needed to enable HTML output and set a custom From name and address. The same pattern integrates cleanly with WooCommerce order hooks or any other WordPress event.
Problem: WordPress sends plain-text, unbranded emails. You need to send styled HTML emails with a consistent header, footer, and variable content from a plugin or theme — without installing a dedicated email plugin.
Solution: Create a PHP email template file, load it with ob_start() / ob_get_clean(), enable HTML content type via the wp_mail_content_type filter, and send with wp_mail().
The helper function that loads the template and sends the email — add to functions.php or a plugin file:
<?php
function send_custom_email( $to, $subject, $template_file, $variables = [] ) {
if ( ! file_exists( $template_file ) ) {
return false;
}
// Make variables available inside the template
extract( $variables, EXTR_SKIP );
// Capture the template output
ob_start();
include $template_file;
$message = ob_get_clean();
// Enable HTML and restore immediately after sending
add_filter( 'wp_mail_content_type', function () { return 'text/html'; } );
$headers = [
'From: ' . get_bloginfo( 'name' ) . ' ',
];
$result = wp_mail( $to, $subject, $message, $headers );
// Restore plain text to avoid affecting other mail sent in the same request
remove_all_filters( 'wp_mail_content_type' );
add_filter( 'wp_mail_content_type', function () { return 'text/plain'; } );
return $result;
}
The email template file — save as get_template_directory() . '/email-templates/order-notify.php'. Uses table-based layout with inline styles for cross-client compatibility:
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"></head>
<body style="margin:0;padding:0;background:#f4f4f4;font-family:Arial,sans-serif;">
<table width="100%" cellpadding="0" cellspacing="0" border="0">
<tr>
<td align="center" style="padding:40px 0;">
<table width="600" cellpadding="0" cellspacing="0" border="0"
style="background:#ffffff;border-radius:4px;padding:40px;">
<tr>
<td style="font-size:24px;font-weight:bold;color:#333;padding-bottom:20px;">
<?php echo esc_html( get_bloginfo( 'name' ) ); ?>
</td>
</tr>
<tr>
<td style="font-size:16px;color:#555;line-height:1.6;padding-bottom:20px;">
<p>Hello <?php echo esc_html( $customer_name ); ?>,</p>
<p>Your order <strong>#<?php echo esc_html( $order_id ); ?></strong> has been received.
Total: <strong><?php echo esc_html( $order_total ); ?></strong></p>
<p><a href="<?php echo esc_url( $order_url ); ?>"
style="background:#0073aa;color:#fff;padding:10px 20px;
text-decoration:none;border-radius:3px;">View Order</a></p>
</td>
</tr>
<tr>
<td style="font-size:12px;color:#999;border-top:1px solid #eee;padding-top:20px;">
© <?php echo date('Y'); ?> <?php echo esc_html( get_bloginfo( 'name' ) ); ?>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
Usage — send the email from any hook, for example on WooCommerce order creation:
<?php
add_action( 'woocommerce_thankyou', 'send_custom_order_email', 10, 1 );
function send_custom_order_email( $order_id ) {
$order = wc_get_order( $order_id );
if ( ! $order ) return;
send_custom_email(
$order->get_billing_email(),
'Order Confirmation #' . $order_id,
get_template_directory() . '/email-templates/order-notify.php',
[
'customer_name' => $order->get_billing_first_name(),
'order_id' => $order_id,
'order_total' => $order->get_formatted_order_total(),
'order_url' => $order->get_view_order_url(),
]
);
}
NOTE: The wp_mail_content_type filter applies globally for the current PHP request. Always restore it to text/plain immediately after sending your HTML email — otherwise any subsequent wp_mail() call in the same request (from other plugins or WordPress core) will attempt to send HTML and may produce malformed output.