Rendering

Themes

Give lttr your colours, font stacks and logo with theme.Theme, and see where each value ends up in the email.

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).

theme/theme.go
type Theme struct {
    Light Light
    Dark  Dark
    Fonts Fonts
    Brand Brand
}

Write a theme

This is the Quiet theme from examples/quiet/theme.go:

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.

FieldUsed for
InkHeadings, pull quotes, table and receipt cells, notice titles, the signature name, product names and prices, F1 nav links
BodyParagraphs, meta values, notice text, signature text
SecondaryEyebrows, table headers, meta labels, sub-lines (such as ≈ US$), muted spans, the H3 masthead, footer text
StrongBodyLetter text, links in the footer
ActionThe pill button, links in prose and :link actions, teal spans, the figure amount, completed progress steps
PillTextThe pill button's label
AccentOrange eyebrows (tone="orange"), story eyebrows, the current progress step
HairlineDividers, the lines between tiles, future progress steps, the banner's bottom border
RowRuleThe rule between receipt and table rows
NoticeBorderThe border of a default notice
GroundThe footer background, and the placeholder ground of an image or tile without a tone
CanvasThe page background
WarnBg, WarnBorder, WarnTextA warn notice
OutOfStock"Out of stock" in the stock module
BannerBg, BannerTextThe H2 banner
Warm, Sea, Map, SunPlaceholder 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.

FieldClass 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:

FieldUsed for
SerifHeadings, the figure amount, quotes, the signature name, product prices
SansParagraphs, letters, tables and receipts, buttons, the footer
MonoEyebrows, 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

theme/theme.go
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
}
  • Name is the logo's alt, the plain-text header line (in capitals), the {brand} placeholder and the default sign-off name, The {Name} team (D7).
  • Logo and LogoDark are file names, not URLs. The renderer joins each to Config.AssetBase, so AssetBase: "https://cdn.acme.example/email" and Logo: "logo.png" give https://cdn.acme.example/email/logo.png. See Config and the renderer.
  • LogoSize is 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:

theme/theme.go
func (t Theme) Validate() error

It reports every problem, one per line:

ValueRuleMessage
Every Light and Dark colourExactly # and six hex digits, either casetheme: Light.Ink: invalid colour "red" (want #RRGGBB)
Fonts.Serif, Sans, MonoNon-empty, only letters, digits, space, ,, ' and -theme: Fonts.Sans: empty font stack
Brand.NameNon-emptytheme: Brand.Name: empty
Brand.Logo, LogoDarkNon-empty file names with no /, \, .., whitespace or control characterstheme: Brand.Logo: invalid file name "img/logo.png"
Brand.LogoSize16 to 96theme: 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).

Copyright © 2026