Themes
A theme is everything brand-specific the renderers draw with: a light palette, a dark palette, three font stacks and the brand mark. It is plain data you put in Config.Theme. The library holds no brand colours of its own.
lttr.Theme is an alias for theme.Theme from github.com/sulv-io/lttr/theme, a leaf package the renderers can import (D3).
type Theme struct {
Light Light
Dark Dark
Fonts Fonts
Brand Brand
}
Write a theme
This is the Quiet theme from examples/quiet/theme.go:
func Theme() lttr.Theme {
return lttr.Theme{
Light: theme.Light{
Ink: "#141410", Body: "#52524A", Secondary: "#6B6B61", StrongBody: "#3C3C35",
Action: "#0B4740", PillText: "#FFFFFF", Accent: "#C84A0E",
Hairline: "#E4E4E0", RowRule: "#EDEDEA", NoticeBorder: "#D8D8D3",
Ground: "#F7F7F5", Canvas: "#FFFFFF",
WarnBg: "#FEF5E0", WarnBorder: "#F0AE23", WarnText: "#5A3D04",
OutOfStock: "#9D1411",
BannerBg: "#FDF0E8", BannerText: "#822E09",
Warm: "#F6EDE6", Sea: "#E3F0EE", Map: "#E8F4F3", Sun: "#062320",
},
Dark: theme.Dark{
Canvas: "#121615", Footer: "#1A1F1D", Hairline: "#2A302E",
Headline: "#F2F2F0", Body: "#A8ADAB",
Action: "#8BC9C3", PillText: "#062320",
CurrentStep: "#EC8A50", AccentText: "#F4B58C",
BannerBg: "#2A1A10", BannerText: "#F4B58C",
Placeholder: "#1B201E",
WarnBg: "#2A2210", WarnBorder: "#6B5314", WarnText: "#F0D9A8",
OutOfStock: "#F08A85",
},
Fonts: theme.Fonts{
Serif: "Georgia,'Times New Roman',serif",
Sans: "Arial,Helvetica,sans-serif",
Mono: "'Courier New',Courier,monospace",
},
Brand: theme.Brand{Name: "CaribHubs", Logo: "logo.png", LogoDark: "logo-dark.png", LogoSize: 36},
}
}
Every colour is a theme.Color, a string written #RRGGBB.
Set the light palette
The light palette is written inline into the HTML: style attributes and bgcolor. It is what every client shows first, and what clients without dark-mode support always show.
| Field | Used for |
|---|---|
Ink | Headings, pull quotes, table and receipt cells, notice titles, the signature name, product names and prices, F1 nav links |
Body | Paragraphs, meta values, notice text, signature text |
Secondary | Eyebrows, table headers, meta labels, sub-lines (such as ≈ US$), muted spans, the H3 masthead, footer text |
StrongBody | Letter text, links in the footer |
Action | The pill button, links in prose and :link actions, teal spans, the figure amount, completed progress steps |
PillText | The pill button's label |
Accent | Orange eyebrows (tone="orange"), story eyebrows, the current progress step |
Hairline | Dividers, the lines between tiles, future progress steps, the banner's bottom border |
RowRule | The rule between receipt and table rows |
NoticeBorder | The border of a default notice |
Ground | The footer background, and the placeholder ground of an image or tile without a tone |
Canvas | The page background |
WarnBg, WarnBorder, WarnText | A warn notice |
OutOfStock | "Out of stock" in the stock module |
BannerBg, BannerText | The H2 banner |
Warm, Sea, Map, Sun | Placeholder grounds for tone="warm", sea, map and sun; Map is also the map band's ground |
Set the dark palette
The dark palette only reaches the email through the head <style>: rules under @media (prefers-color-scheme:dark), repeated under [data-ogsc] (text and border colours) and [data-ogsb] (backgrounds) for Outlook.com. Each field feeds one or more dk-* class hooks, and every rule is !important.
| Field | Class hooks |
|---|---|
Canvas | .dk-canvas background |
Footer | .dk-foot background |
Hairline | .dk-off, .dk-hr backgrounds; .dk-bd border; the .dk-ib banner's border |
Headline | .dk-ink, .dk-step-now text |
Body | .dk-body text |
Action | .dk-act text; .dk-pill background and border; .dk-done background |
PillText | .dk-pill-a text |
CurrentStep | .dk-now background |
AccentText | .dk-eo text |
BannerBg, BannerText | .dk-ib background and text |
Placeholder | .dk-ph background |
WarnBg, WarnBorder, WarnText | .dk-warn background, border and text |
OutOfStock | .dk-red text |
The sun placeholder ground carries no dk-ph hook, so it stays dark in dark mode (D32). The dark block also swaps the logos: .logo-lt is hidden and .logo-dk is shown. See HTML output.
Set the fonts
Fonts holds three CSS font stacks:
| Field | Used for |
|---|---|
Serif | Headings, the figure amount, quotes, the signature name, product prices |
Sans | Paragraphs, letters, tables and receipts, buttons, the footer |
Mono | Eyebrows, table headers, code spans and the code module, mono spans, the H2 banner and H3 masthead |
Use fallback stacks that exist on the reader's machine. Web fonts are not loaded. The Outlook VML button always uses Arial,sans-serif, whatever Sans says.
Set the brand
type Brand struct {
Name string // used in header text, logo alt and the default sign-off
Logo string // file name under Config.AssetBase, e.g. "logo.png" (72×72, shown at LogoSize)
LogoDark string // white-chip version for dark mode
LogoSize int // display size in CSS pixels, 16..96
}
Nameis the logo'salt, the plain-text header line (in capitals), the{brand}placeholder and the default sign-off name,The {Name} team(D7).LogoandLogoDarkare file names, not URLs. The renderer joins each toConfig.AssetBase, soAssetBase: "https://cdn.acme.example/email"andLogo: "logo.png"givehttps://cdn.acme.example/email/logo.png. See Config and the renderer.LogoSizeis the square size the logo is shown at. Quiet uses 36 and ships a 72×72 PNG for sharp rendering on high-density screens.
Validate a theme
NewRenderer calls Theme.Validate, and you can call it yourself:
func (t Theme) Validate() error
It reports every problem, one per line:
| Value | Rule | Message |
|---|---|---|
Every Light and Dark colour | Exactly # and six hex digits, either case | theme: Light.Ink: invalid colour "red" (want #RRGGBB) |
Fonts.Serif, Sans, Mono | Non-empty, only letters, digits, space, ,, ' and - | theme: Fonts.Sans: empty font stack |
Brand.Name | Non-empty | theme: Brand.Name: empty |
Brand.Logo, LogoDark | Non-empty file names with no /, \, .., whitespace or control characters | theme: Brand.Logo: invalid file name "img/logo.png" |
Brand.LogoSize | 16 to 96 | theme: Brand.LogoSize: 0 out of range 16..96 |
The rules exist so that nothing from a theme can break out of the CSS or markup it is written into. Once a theme validates, the renderers treat it as trusted.
Know what a theme cannot change
Sizes are not part of the theme. The type scale (headings 40/44, body 17/25.5, footer 12/19, …), the 600px frame, the 56px side padding (24px under 480px), paddings, radii and the pill's 48px height are library constants in render/html/tokens.go. They are the layout system; the theme is the brand. Spacing between modules is fixed too, in layout (see Layout and spacing).