Myrtle is a composable, strongly typed email content builder for Go.
| Default | Terminal |
|---|---|
![]() |
![]() |
- Fluent builder pattern for email content.
- Strongly typed library of blocks.
- Modern built-in themes:
default,flat,terminal,editorial. - Built-in advanced blocks such as tables, charts, grids.
- High-impact blocks: timelines, standout stats rows, badges, attachments.
- Dual rendering APIs:
HTML()for final HTML output.Text()for plain-text fallback output.
- Customizable and extensible: bring your own theme, styles or custom blocks.
- Left-to-right and right-to-left direction support (e.g. for Arabic/Hebrew).
- Renders OK in Outlook Classic and other notoriously difficult email clients.
- Dependency-free aside from
goldmarkfor Markdown rendering.
go get github.com/gzuidhof/myrtlepackage main
import (
"github.com/gzuidhof/myrtle"
defaulttheme "github.com/gzuidhof/myrtle/theme/default"
)
func main() {
email := myrtle.NewBuilder(defaulttheme.New()).
WithPreheader("Use this one-time code to sign in").
AddHeading("Your verification code").
AddText("Use the code below to complete your sign-in. This code expires in 10 minutes.").
AddVerificationCode("Verification code", "493817").
AddKeyValue("Request details", []myrtle.KeyValuePair{
{Key: "IP", Value: "203.0.113.5"},
{Key: "Location", Value: "Amsterdam, NL"},
}).
AddText("If you did not request this code, secure your account immediately.").
AddButton("Review account", "https://example.com/account/security").
Build()
html, err := email.HTML()
if err != nil {
panic(err)
}
md, err := email.Text()
if err != nil {
panic(err)
}
// Use your favorite e-mail sending library to send the email with the generated HTML and text content.
// ...
_ = html
_ = md
}Make use of auto-complete/Intellisense in your IDE to explore the rich library of blocks and customization options.
Custom block (basic)
package main
import (
"fmt"
"github.com/gzuidhof/myrtle"
"github.com/gzuidhof/myrtle/theme"
defaulttheme "github.com/gzuidhof/myrtle/theme/default"
)
type DeploymentStatus struct {
Service string
Version string
Status string
}
func main() {
block := myrtle.NewCustomBlock(
theme.BlockKind("deployment_status"),
DeploymentStatus{Service: "billing-api", Version: "v1.42.0", Status: "healthy"},
func(v DeploymentStatus, values theme.Values) (string, error) {
_ = values
return fmt.Sprintf("<p><strong>%s</strong> on <code>%s</code>: %s</p>", v.Service, v.Version, v.Status), nil
},
func(v DeploymentStatus, context myrtle.RenderContext) (string, error) {
_ = context
return fmt.Sprintf("%s on %s: %s", v.Service, v.Version, v.Status), nil
},
)
email := myrtle.NewBuilder(defaulttheme.New()).
AddHeading("Deployment update").
Add(block).
Build()
_, _ = email.HTML()
_, _ = email.Text()
}Style tweaks (basic)
package main
import (
"github.com/gzuidhof/myrtle"
"github.com/gzuidhof/myrtle/theme"
defaulttheme "github.com/gzuidhof/myrtle/theme/default"
)
func main() {
styles := theme.DefaultDarkModeStyles()
styles.ColorPrimary = "#22d3ee"
styles.MaxWidthMain = "640px"
styles.MainContentBodyTopSpacing = "0"
email := myrtle.NewBuilder(
defaulttheme.New(),
myrtle.WithStyles(styles),
).
WithPreheader("Theme overrides example").
AddHeading("Style tweaks").
AddText("This message uses a dark preset with a few token overrides.").
AddButton("Open dashboard", "https://example.com/dashboard").
Build()
_, _ = email.HTML()
_, _ = email.Text()
}Concurrent rendering with shared header/footer/theme/styles
package main
import (
"sync"
"github.com/gzuidhof/myrtle"
"github.com/gzuidhof/myrtle/theme"
defaulttheme "github.com/gzuidhof/myrtle/theme/default"
)
type RenderedEmail struct {
To string
HTML string
Text string
Err error
}
func main() {
// Shared theme/styles/header/footer used to build one baseline builder.
sharedStyles := theme.Styles{
ColorPrimary: "#2563eb",
MaxWidthMain: "640px",
}
sharedHeader := myrtle.NewGroup().
AddImage("https://example.com/logo.png", "Myrtle", myrtle.ImageWidth(120), myrtle.ImageAlign(myrtle.ImageAlignmentCenter)).
AddText("Security updates", myrtle.TextAlign(myrtle.TextAlignCenter), myrtle.TextWeight(myrtle.TextWeightSemibold))
sharedFooter := myrtle.NewGroup().
AddLegal(
"Myrtle Inc.",
"Dam Square 1, 1012 JS Amsterdam, Netherlands",
"https://example.com/preferences",
"https://example.com/unsubscribe",
)
baseBuilder := myrtle.NewBuilder(defaulttheme.New(), myrtle.WithStyles(sharedStyles)).
WithHeader(sharedHeader).
WithFooter(sharedFooter).
WithPreheader("Important account security update")
recipients := []string{"ana@example.com", "bo@example.com", "cy@example.com"}
results := make([]RenderedEmail, len(recipients))
var wg sync.WaitGroup
for i, to := range recipients {
wg.Add(1)
go func(i int, to string) {
defer wg.Done()
// Clone the baseline builder and apply recipient-specific content.
email := baseBuilder.Clone().
AddHeading("Account alert").
AddText("We detected a sign-in from a new location.").
AddKeyValue("Recipient", []myrtle.KeyValuePair{{Key: "Email", Value: to}}).
AddButton("Review activity", "https://example.com/security").
Build()
html, err := email.HTML()
if err != nil {
results[i] = RenderedEmail{To: to, Err: err}
return
}
text, err := email.Text()
results[i] = RenderedEmail{To: to, HTML: html, Text: text, Err: err}
}(i, to)
}
wg.Wait()
_ = results
}- example/weekly_operations_brief.go
- example/account_deletion_confirmation.go
- example/security.go
- example/monster.go
| Default | Flat |
|---|---|
![]() |
![]() |
| Terminal | Editorial |
![]() |
![]() |
| Default | Flat |
|---|---|
![]() |
![]() |
| Terminal | Editorial |
![]() |
![]() |
| Default | Flat |
|---|---|
![]() |
![]() |
| Terminal | Editorial |
![]() |
![]() |
The monster example is a fun showcase of many blocks and styles together. It intentionally has a lot of content to demonstrate how the builder and themes handle it.
The example/server package serves a directory of all example emails and block previews.
Clone this repository and run the server to preview example emails in the browser at http://localhost:8380/.
go run ./example/server/cmdThe code for this repository is repetitive and verbose, I recommend you use AI-assisted code generation to speed up development. Writing inlined CSS manually is particularly painful.
This library is not stable yet, your layouts will likely shift a bit with future releases.
Myrtle is licensed under the MIT License. See LICENSE for more information.
Myrtle she wrote.












