/ Content
Laravel's mail system is one of those features that works great out of the box until you need to customize it. The default templates are fine for password resets and verification emails, but eventually a client asks for branded emails, or you need to send notifications that look different from your transactional mail, or you want users to customize the colors in emails your app sends on their behalf.
The mail documentation covers the basics well. What it doesn't cover is how all the pieces fit together when you need real control over your email templates.
The Component Hierarchy
Two mail components matter most: <x-mail::message> and <x-mail::layout>.
<x-mail::message> is what you see in every example. It wraps your content in a full email template with a header showing your app name and a footer with a copyright line. You write Markdown inside it and Laravel renders everything into a responsive HTML email.
<x-mail::message>
# Hi {{ $user->first_name }}!
Visit your settings page by clicking the button below.
<x-mail::button :url="route('user.settings')">
Your Settings
</x-mail::button>
Thanks,<br>
Team {{ config('app.name') }}
</x-mail::message>
<x-mail::layout> is the parent component that <x-mail::message> uses internally. It provides the base HTML structure with named slots for the header, footer, subcopy, and main content. If you want full control over every part of the email, you use the layout directly:
<x-mail::layout>
<x-slot:header>
<x-mail::header :url="config('app.url')">
<img src="{{ asset('images/logo.png') }}" alt="{{ config('app.name') }}">
</x-mail::header>
</x-slot:header>
# Your Custom Content Here
Whatever markdown you want.
<x-slot:footer>
<x-mail::footer>
© {{ date('Y') }} {{ config('app.name') }}. All rights reserved.
</x-mail::footer>
</x-slot:footer>
</x-mail::layout>
Understanding this hierarchy matters because it determines how deep you need to go with customization.
Customization Approaches
There are several ways to customize mail templates, each with different tradeoffs.
Option 1: Use the base components as-is. For internal tools or MVPs, the default <x-mail::message> works fine. Don't overthink it.
Option 2: Publish and modify Laravel's components. Run php artisan vendor:publish --tag=laravel-mail and edit the files in resources/views/vendor/mail. This gives you control but ties you to Laravel's component structure. Future Laravel updates might introduce changes you'll need to merge manually.
Option 3: Use <x-mail::layout> with custom header and footer. Skip the message component entirely and build on the layout. You get control over header and footer without touching vendor files, but you repeat this setup in every mailable.
Option 4: Create your own message component. Build a component that wraps <x-mail::layout> with your branding baked in. This is my preference for most projects. You get reusability without modifying vendor code.
Here's what that looks like:
{{-- resources/views/components/mail/branded-message.blade.php --}}
<x-mail::layout>
<x-slot:header>
<x-mail::header :url="config('app.url')">
<img src="{{ asset('images/email-logo.png') }}" alt="{{ config('app.name') }}" height="40">
</x-mail::header>
</x-slot:header>
{{ $slot }}
<x-slot:subcopy>
@isset($subcopy)
{{ $subcopy }}
@endisset
</x-slot:subcopy>
<x-slot:footer>
<x-mail::footer>
© {{ date('Y') }} {{ config('app.name') }}<br>
123 Business Street, City, State 12345
</x-mail::footer>
</x-slot:footer>
</x-mail::layout>
Then use it in your mailables:
<x-mail.branded-message>
# Order Shipped
Your order #{{ $order->number }} has shipped!
<x-mail::button :url="$trackingUrl">
Track Package
</x-mail::button>
</x-mail.branded-message>
Notifications Are Different
Notifications and Mailables look similar but work differently under the hood. A Mailable uses a Blade view directly. A Notification's toMail method returns a MailMessage object that gets rendered through a different template.
The MailMessage class has methods like line(), action(), and greeting() that build up content programmatically:
public function toMail(object $notifiable): MailMessage
{
return (new MailMessage)
->greeting('Hello!')
->line('One of your invoices has been paid.')
->lineIf($this->amount > 0, "Amount paid: {$this->amount}")
->action('View Invoice', url('/invoice/'.$this->invoice->id))
->line('Thank you for using our application!');
}
Behind the scenes, MailMessage has a default markdown template of notifications::email. That template renders all the lines and actions you've chained together.
You can also use a custom markdown view with the markdown() method:
public function toMail(object $notifiable): MailMessage
{
return (new MailMessage)
->subject('Invoice Paid')
->markdown('mail.invoice.paid', [
'url' => url('/invoice/'.$this->invoice->id),
'invoice' => $this->invoice,
]);
}
When you call markdown(), the MailMessage class ignores all your line() and action() calls. It uses your view instead, passing only the data you provide in the second argument.
There's also view() for plain Blade templates without Markdown processing:
public function toMail(object $notifiable): MailMessage
{
return (new MailMessage)
->subject('Invoice Paid')
->view('mail.invoice.paid', [
'invoice' => $this->invoice,
]);
}
One important gotcha: Laravel's <x-mail::*> components only work in Markdown views. If you use view() instead of markdown(), trying to render <x-mail::button> will throw a component not found error. The Markdown renderer is what makes those components available.
Customizing Notification Templates
Three ways to customize notification templates:
Publish the mail components: php artisan vendor:publish --tag=laravel-mail gives you the same components used by mailables. Edit them in resources/views/vendor/mail.
Publish the notification template: php artisan vendor:publish --tag=laravel-notifications publishes the notifications::email template to resources/views/vendor/notifications. This is the template that renders MailMessage objects.
Use a custom template: The template() method on MailMessage lets you specify your own template:
public function toMail(object $notifiable): MailMessage
{
return (new MailMessage)
->template('emails.notifications.branded')
->greeting('Hello!')
->line('One of your invoices has been paid.')
->action('View Invoice', url('/invoice/'.$this->invoice->id))
->line('Thank you!');
}
Your custom template receives the same variables as the default: $greeting, $introLines, $actionText, $actionUrl, $outroLines, and so on. Copy the published notifications::email template as a starting point and modify it to match your branding.
Theming
Laravel's mail theming system lets you swap CSS files without touching your templates. The default theme lives in the published resources/views/vendor/mail/html/themes/default.css file.
To create a custom theme, add a new CSS file to the themes directory:
/* resources/views/vendor/mail/html/themes/corporate.css */
.button-primary {
background-color: #1a365d;
border-bottom: 8px solid #1a365d;
border-left: 18px solid #1a365d;
border-right: 18px solid #1a365d;
border-top: 8px solid #1a365d;
}
Then specify the theme in your Mailable:
class OrderShipped extends Mailable
{
public $theme = 'corporate';
// ...
}
For notifications, use the theme() method:
public function toMail(object $notifiable): MailMessage
{
return (new MailMessage)
->theme('corporate')
->greeting('Hello!')
->line('Your order has shipped.');
}
You can also set the default theme in config/mail.php:
'markdown' => [
'theme' => 'corporate',
'paths' => [
resource_path('views/vendor/mail'),
],
],
The paths array tells Laravel where to look for theme files. By default it points to the published vendor directory, but you can change it to keep themes elsewhere.
Dynamic Theming with Blade
Here's where it gets interesting. Theme files don't have to be CSS. They can be Blade templates.
Say you're building a platform where businesses send emails to their customers through your app. Each business has their own brand colors. You can create a theme template that uses variables:
/* resources/views/vendor/mail/html/themes/dynamic.blade.php */
.button-primary {
background-color: {{ $buttonColor ?? '#3490dc' }};
border-bottom: 8px solid {{ $buttonColor ?? '#3490dc' }};
border-left: 18px solid {{ $buttonColor ?? '#3490dc' }};
border-right: 18px solid {{ $buttonColor ?? '#3490dc' }};
border-top: 8px solid {{ $buttonColor ?? '#3490dc' }};
}
.header a {
color: {{ $headerColor ?? '#333333' }};
}
Name the file with a .blade.php extension but reference it without the extension:
public function toMail(object $notifiable): MailMessage
{
$business = $this->invoice->business;
return (new MailMessage)
->theme('dynamic')
->subject('Invoice Paid')
->markdown('mail.invoice.paid', [
'invoice' => $this->invoice,
'buttonColor' => $business->primary_color ?? '#3490dc',
'headerColor' => $business->secondary_color ?? '#333333',
]);
}
The view data you pass to markdown() is available in the theme template. This lets users customize email appearance without you maintaining separate theme files for each customer.
Previewing Emails
When you're iterating on email designs, sending actual emails is slow. Laravel lets you return mail messages directly from routes for quick previewing:
Route::get('/preview/invoice-paid', function () {
$invoice = Invoice::factory()->make();
return (new InvoicePaid($invoice))
->toMail($invoice->user);
});
For Mailables:
Route::get('/preview/order-shipped', function () {
$order = Order::factory()->make();
return new OrderShipped($order);
});
Wrap these in environment checks or auth middleware so they don't end up in production.
Putting It Together
For most projects, I end up with this structure:
- A custom message component (
<x-mail.branded-message>) that wraps<x-mail::layout>with my header and footer - A custom notification template that mirrors the branding
- The default theme modified with brand colors
- Preview routes in local and staging environments
The mail system has more flexibility than the docs suggest. Once you understand how the components nest and where the template boundaries are, you can customize nearly anything without fighting the framework.