Send Custom HTML Email Templates in WordPress with wp_mail()

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.