The parts of an HTML email: inline styles and table layout.
<email.Button href={gx.URL("/orders/1042")}>See the order</email.Button>Installation
Run the command in the app module.
gx add emailThe command writes these files.
| File | Path in the app |
|---|---|
Document.gx |
ui/email/Document.gx |
Section.gx |
ui/email/Section.gx |
Row.gx |
ui/email/Row.gx |
Column.gx |
ui/email/Column.gx |
Text.gx |
ui/email/Text.gx |
Heading.gx |
ui/email/Heading.gx |
Button.gx |
ui/email/Button.gx |
Image.gx |
ui/email/Image.gx |
Divider.gx |
ui/email/Divider.gx |
Document.fixtures.go |
ui/email/Document.fixtures.go |
Section.fixtures.go |
ui/email/Section.fixtures.go |
Row.fixtures.go |
ui/email/Row.fixtures.go |
Column.fixtures.go |
ui/email/Column.fixtures.go |
Text.fixtures.go |
ui/email/Text.fixtures.go |
Heading.fixtures.go |
ui/email/Heading.fixtures.go |
Button.fixtures.go |
ui/email/Button.fixtures.go |
Image.fixtures.go |
ui/email/Image.fixtures.go |
Divider.fixtures.go |
ui/email/Divider.fixtures.go |
styles.go |
ui/email/styles.go |
Copy each file to its path in the app. Change each import of a registry package to the path of that package in the app.
package email
props {
// Preview is the text that an inbox shows after the subject. It is not visible in the email.
Preview string = ""
// Width is the largest width of the content in pixels.
Width int = 600
// Children is the sections of the email.
Children gx.Node
}
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:var(--background)">
<tbody>
<tr>
<td align="center" style="padding:24px 12px">
if p.Preview != "" {
<div style="display:none;max-height:0;overflow:hidden;opacity:0">{p.Preview}</div>
}
<table role="presentation" width={p.width()} cellpadding="0" cellspacing="0" border="0" style={p.style()}>
<tbody>
<tr>
<td align="left">{p.Children}</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>package email
props {
// Card gives the section a border, a background and padding.
Card bool = false
// Children is the content of the section.
Children gx.Node
}
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
<tbody>
<tr>
<td style={p.style()}>{p.Children}</td>
</tr>
</tbody>
</table>package email
props {
// Children is the columns of the row.
Children gx.Node
}
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
<tbody>
<tr>{p.Children}</tr>
</tbody>
</table>package email
props {
// Width is the width of the column in pixels. Zero shares the width of the row.
Width int = 0
// Align is the horizontal position of the content: Left, Center or Right.
Align Align = Left
// Children is the content of the column.
Children gx.Node
}
<td align={p.Align.attr()} style={p.style()}>{p.Children}</td>package email
props {
// Muted gives the text a smaller size and a weaker colour, for a note.
Muted bool = false
// Align is the horizontal position of the text: Left, Center or Right.
Align Align = Left
// Children is the text.
Children gx.Node
}
<p style={p.style()}>{p.Children}</p>package email
props {
// Level is the level of the heading: 1, 2 or 3.
Level int = 1
// Align is the horizontal position of the heading: Left, Center or Right.
Align Align = Left
// Children is the text of the heading.
Children gx.Node
}
switch p.level() {
case 2:
<h2 style={p.style()}>{p.Children}</h2>
case 3:
<h3 style={p.style()}>{p.Children}</h3>
default:
<h1 style={p.style()}>{p.Children}</h1>
}package email
props {
// Href is the address that the button opens. gx.RenderEmail makes a site path an absolute URL.
Href gx.URL
// Align is the horizontal position of the button: Left, Center or Right.
Align Align = Left
// Children is the label of the button.
Children gx.Node
}
<table role="presentation" align={p.Align.attr()} cellpadding="0" cellspacing="0" border="0" style="margin:0 0 16px">
<tbody>
<tr>
<td align="center" style="background-color:var(--primary);border-radius:var(--radius)">
<a href={p.Href} style="display:inline-block;padding:12px 20px;font-family:Helvetica,Arial,sans-serif;font-size:16px;line-height:24px;font-weight:700;color:var(--primary-foreground);text-decoration:none">{p.Children}</a>
</td>
</tr>
</tbody>
</table>package email
props {
// Src is the address of the image. gx.RenderEmail makes a site path an absolute URL.
Src gx.URL
// Alt is the text of the image. An email client shows it when it blocks images.
Alt string
// Width is the width of the image in pixels.
Width int
// Height is the height of the image in pixels.
Height int
}
<img src={p.Src} alt={p.Alt} width={size(p.Width)} height={size(p.Height)} style={p.style()} />package email
<hr style="border:0;border-top:1px solid var(--border);margin:24px 0" />package email
import "github.com/alternayte/gx"
// DocumentFixtures are the examples of Document in the dev gallery.
var DocumentFixtures = gx.Fixtures[DocumentProps]{
"Default": {Children: Text(TextProps{Children: gx.Text("The content of the email is here.")})},
"Preview": {Preview: "Your order is on its way.", Width: 480, Children: Text(TextProps{Children: gx.Text("An inbox shows the preview text after the subject.")})},
}package email
import "github.com/alternayte/gx"
// SectionFixtures are the examples of Section in the dev gallery.
var SectionFixtures = gx.Fixtures[SectionProps]{
"Default": {Children: Text(TextProps{Children: gx.Text("A section groups the parts of an email.")})},
"Card": {Card: true, Children: Text(TextProps{Children: gx.Text("A card section has a border and padding.")})},
}package email
import "github.com/alternayte/gx"
// RowFixtures are the examples of Row in the dev gallery.
var RowFixtures = gx.Fixtures[RowProps]{
"TwoColumns": {Children: gx.Frag(
Column(ColumnProps{Children: gx.Text("Tea, 2 boxes")}),
Column(ColumnProps{Width: 120, Align: Right, Children: gx.Text("12.00")}),
)},
}package email
import "github.com/alternayte/gx"
// ColumnFixtures are the examples of Column in the dev gallery.
var ColumnFixtures = gx.Fixtures[ColumnProps]{
"Default": {Children: gx.Text("A column shares the width of the row.")},
"Fixed": {Width: 160, Children: gx.Text("160 pixels")},
"Right": {Align: Right, Children: gx.Text("To the right")},
}
// ColumnWrap renders the column inside a row, as an email uses it.
func ColumnWrap(n gx.Node) gx.Node {
return Row(RowProps{Children: n})
}package email
import "github.com/alternayte/gx"
// TextFixtures are the examples of Text in the dev gallery.
var TextFixtures = gx.Fixtures[TextProps]{
"Default": {Children: gx.Text("Thank you for your order. We send it in two days.")},
"Muted": {Muted: true, Children: gx.Text("You get this email because you have an account.")},
"Center": {Align: Center, Children: gx.Text("Text in the centre")},
}package email
import "github.com/alternayte/gx"
// HeadingFixtures are the examples of Heading in the dev gallery.
var HeadingFixtures = gx.Fixtures[HeadingProps]{
"One": {Children: gx.Text("Your order is on its way")},
"Two": {Level: 2, Children: gx.Text("What you ordered")},
"Three": {Level: 3, Align: Center, Children: gx.Text("Questions")},
}package email
import "github.com/alternayte/gx"
// ButtonFixtures are the examples of Button in the dev gallery.
var ButtonFixtures = gx.Fixtures[ButtonProps]{
"Default": {Href: gx.URL("/orders/1042"), Children: gx.Text("See the order")},
"Center": {Href: gx.URL("/orders/1042"), Align: Center, Children: gx.Text("Follow the parcel")},
}package email
import "github.com/alternayte/gx"
// pixel is a grey image of one pixel, so the gallery needs no file.
const pixel = gx.URL("data:image/gif;base64,R0lGODlhAQABAIAAAMzMzAAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==")
// ImageFixtures are the examples of Image in the dev gallery.
var ImageFixtures = gx.Fixtures[ImageProps]{
"Default": {Src: pixel, Alt: "The logo of the shop", Width: 120, Height: 40},
}package email
import "github.com/alternayte/gx"
// DividerFixtures are the examples of Divider in the dev gallery.
var DividerFixtures = gx.Fixtures[DividerProps]{
"Default": {},
}// Package email holds the parts of an HTML email: inline styles and table
// layout, which each email client reads. A component of this package has no
// class, no signal and no script (GX6010). The styles read the theme tokens
// with var(); gx.RenderEmail gives each one its value.
package email
import (
"strconv"
"github.com/alternayte/gx"
)
// Align is the horizontal position of the content of a part.
type Align string
// The positions of the content.
const (
Left Align = "left"
Center Align = "center"
Right Align = "right"
)
// attr returns the value of the align attribute; a zero value is Left.
func (a Align) attr() string {
switch a {
case Center, Right:
return string(a)
}
return string(Left)
}
// font is the font of each text. An email client has no font of the site.
const font = "font-family:Helvetica,Arial,sans-serif;"
// width returns the width of the content in pixels; a zero value is 600.
func (p DocumentProps) width() string {
if p.Width <= 0 {
return "600"
}
return strconv.Itoa(p.Width)
}
// style returns the style of the content table.
func (p DocumentProps) style() gx.Style {
return gx.Style("width:100%;max-width:" + p.width() + "px")
}
// style returns the style of the cell of a section.
func (p SectionProps) style() gx.Style {
if p.Card {
return "padding:24px;background-color:var(--card);border:1px solid var(--border);border-radius:var(--radius)"
}
return "padding:12px 0"
}
// style returns the style of a column. An empty width shares the row.
func (p ColumnProps) style() gx.Style {
s := "padding:0 8px 0 0;vertical-align:top;" + font + "font-size:16px;line-height:24px;color:var(--foreground)"
if p.Width > 0 {
s += ";width:" + strconv.Itoa(p.Width) + "px"
}
return gx.Style(s)
}
// style returns the style of a paragraph.
func (p TextProps) style() gx.Style {
s := "margin:0 0 16px;" + font + "font-size:16px;line-height:24px;text-align:" + p.Align.attr()
if p.Muted {
return gx.Style(s + ";font-size:14px;line-height:20px;color:var(--muted-foreground)")
}
return gx.Style(s + ";color:var(--foreground)")
}
// level returns the level of a heading: 1, 2 or 3. A zero value is 1.
func (p HeadingProps) level() int {
if p.Level < 1 {
return 1
}
if p.Level > 3 {
return 3
}
return p.Level
}
// style returns the style of a heading.
func (p HeadingProps) style() gx.Style {
size := map[int]string{1: "font-size:28px;line-height:36px", 2: "font-size:22px;line-height:28px", 3: "font-size:18px;line-height:24px"}[p.level()]
return gx.Style("margin:0 0 16px;" + font + size + ";font-weight:700;color:var(--foreground);text-align:" + p.Align.attr())
}
// size returns a pixel count as an attribute value.
func size(n int) string { return strconv.Itoa(n) }
// style returns the style of an image. The image is not wider than its
// column.
func (p ImageProps) style() gx.Style {
return "display:block;border:0;outline:none;max-width:100%;height:auto"
}The theme must define these tokens: --background, --foreground, --card, --primary, --primary-foreground, --muted-foreground, --border, --radius.
Usage
The parts of an HTML email. Each part has inline styles and table layout, which each email client reads. No part has a class, a signal or a script.
Write the email as a component in a package with the name email. Then render it with gx.RenderEmail.
<email.Document preview="Your order is on its way.">
<email.Section>
<email.Heading>Your order is on its way</email.Heading>
<email.Text>Thank you for your order.</email.Text>
<email.Button href={gx.URL(route.Order{ID: p.ID}.URL())}>See the order</email.Button>
</email.Section>
<email.Divider />
<email.Section card>
<email.Row>
<email.Column>Tea, 2 boxes</email.Column>
<email.Column width={120} align={email.Right}>12.00</email.Column>
</email.Row>
</email.Section>
<email.Image src={gx.URL("/logo.png")} alt="The logo of the shop" width={120} height={40} />
<email.Text muted>You get this email because you have an account.</email.Text>
</email.Document>msg, err := gx.RenderEmail(OrderEmail(props), gx.EmailOptions{
BaseURL: "https://shop.example",
Tokens: gxstyles.LightTokens(),
})msg.HTML is the HTML document and msg.Text is the same content as plain text. Give the two parts to your mail library. Gx sends no email.
Parts
| Part | Use |
|---|---|
Document |
The background and the centre column. It is the root of each email. preview is the text that an inbox shows after the subject. |
Section |
A group of parts. card gives it a border and padding. |
Row |
One row of columns. |
Column |
One cell of a row. width is in pixels; zero shares the row. |
Text |
A paragraph. muted is for a note. |
Heading |
A heading of level 1, 2 or 3. |
Button |
A link with the look of a button. |
Image |
An image with its size and its alt text. |
Divider |
A line between two sections. |
The styles read the theme tokens with var(): --background, --foreground, --card, --primary, --primary-foreground, --muted-foreground, --border and --radius. gx.RenderEmail gives each one the value from gxstyles.LightTokens() or gxstyles.DarkTokens(), so the email has no CSS variable.
Examples
Button: Default
<email.Button href={gx.URL("/orders/1042")}>See the order</email.Button>Button: Center
<email.Button href={gx.URL("/orders/1042")} align={email.Center}>Follow the parcel</email.Button>Column: Default
<email.Column>A column shares the width of the row.</email.Column>Column: Fixed
<email.Column width={160}>160 pixels</email.Column>Column: Right
<email.Column align={email.Right}>To the right</email.Column>Divider: Default
<email.Divider />Document: Default
<email.Document><email.Text>The content of the email is here.</email.Text></email.Document>Document: Preview
<email.Document preview="Your order is on its way." width={480}>
<email.Text>An inbox shows the preview text after the subject.</email.Text>
</email.Document>Heading: One
<email.Heading>Your order is on its way</email.Heading>Heading: Two
<email.Heading level={2}>What you ordered</email.Heading>Heading: Three
<email.Heading level={3} align={email.Center}>Questions</email.Heading>Image: Default
<email.Image src={gx.URL("data:image/gif;base64,R0lGODlhAQABAIAAAMzMzAAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==")} alt="The logo of the shop" width={120} height={40} />Row: Two columns
<email.Row>
<email.Column>Tea, 2 boxes</email.Column>
<email.Column width={120} align={email.Right}>12.00</email.Column>
</email.Row>Section: Default
<email.Section><email.Text>A section groups the parts of an email.</email.Text></email.Section>Section: Card
<email.Section card><email.Text>A card section has a border and padding.</email.Text></email.Section>Text: Default
<email.Text>Thank you for your order. We send it in two days.</email.Text>Text: Muted
<email.Text muted>You get this email because you have an account.</email.Text>Text: Center
<email.Text align={email.Center}>Text in the centre</email.Text>API reference
A tag sets a prop by its name with a lower-case first letter: Class is class.
email.Button
| Prop | Type | Default | Description |
|---|---|---|---|
Href |
gx.URL |
Required | Href is the address that the button opens. gx.RenderEmail makes a site path an absolute URL. |
Align |
Align |
Left |
Align is the horizontal position of the button: Left, Center or Right. |
Children |
gx.Node |
Required | Children is the label of the button. |
email.Column
| Prop | Type | Default | Description |
|---|---|---|---|
Width |
int |
0 |
Width is the width of the column in pixels. Zero shares the width of the row. |
Align |
Align |
Left |
Align is the horizontal position of the content: Left, Center or Right. |
Children |
gx.Node |
Required | Children is the content of the column. |
email.Divider
The component has no props.
email.Document
| Prop | Type | Default | Description |
|---|---|---|---|
Preview |
string |
"" |
Preview is the text that an inbox shows after the subject. It is not visible in the email. |
Width |
int |
600 |
Width is the largest width of the content in pixels. |
Children |
gx.Node |
Required | Children is the sections of the email. |
email.Heading
| Prop | Type | Default | Description |
|---|---|---|---|
Level |
int |
1 |
Level is the level of the heading: 1, 2 or 3. |
Align |
Align |
Left |
Align is the horizontal position of the heading: Left, Center or Right. |
Children |
gx.Node |
Required | Children is the text of the heading. |
email.Image
| Prop | Type | Default | Description |
|---|---|---|---|
Src |
gx.URL |
Required | Src is the address of the image. gx.RenderEmail makes a site path an absolute URL. |
Alt |
string |
Required | Alt is the text of the image. An email client shows it when it blocks images. |
Width |
int |
Required | Width is the width of the image in pixels. |
Height |
int |
Required | Height is the height of the image in pixels. |
email.Row
| Prop | Type | Default | Description |
|---|---|---|---|
Children |
gx.Node |
Required | Children is the columns of the row. |
email.Section
| Prop | Type | Default | Description |
|---|---|---|---|
Card |
bool |
false |
Card gives the section a border, a background and padding. |
Children |
gx.Node |
Required | Children is the content of the section. |
email.Text
| Prop | Type | Default | Description |
|---|---|---|---|
Muted |
bool |
false |
Muted gives the text a smaller size and a weaker colour, for a note. |
Align |
Align |
Left |
Align is the horizontal position of the text: Left, Center or Right. |
Children |
gx.Node |
Required | Children is the text. |
Do and do not
Do
- Put each part inside one
Document. - Put only
Columnparts in aRow. - Give each
Imageitsalttext, its width and its height. Many email clients block images at first. - Use a site path or a typed link for an address.
gx.RenderEmailmakes it an absolute URL.
Don't
- Do not add a
classattribute or a signal to a part. The compiler reports GX6010. - Do not use a component of a page, for example
button.Button, in an email. Its classes have no effect there, andgx.RenderEmailreturns an error. - Do not put text directly in a
Row. A table row takes only cells.
Keyboard
| Key | Action |
|---|---|
| None | The parts are static. A link and a button get the focus as each link does. |