Theme
Customize colors and fonts to match your brand.
Customize colors and fonts to match your brand:
const { html } = await render(markdown, {
theme: {
brandColor: '#e11d48',
buttonColor: '#e11d48',
fontFamily: 'Georgia, serif',
}
});Dark Theme
A built-in dark theme is available. Pass it directly to render the email dark for everyone:
import { render, darkTheme } from 'emailmd';
const { html } = await render(markdown, { theme: darkTheme });Or use it as a base and override individual values:
const { html } = await render(markdown, {
theme: { ...darkTheme, brandColor: '#e11d48' },
});You can also activate it per-email via frontmatter; see Frontmatter.
Automatic Dark Mode
To render light by default but adapt to the reader's dark-mode preference, opt in with darkTheme:
// Built-in dark palette
const { html } = await render(markdown, { darkTheme: true });
// Or override parts of it
const { html } = await render(markdown, {
darkTheme: { brandColor: '#22d3ee' },
});Or per-email in frontmatter with theme: auto:
---
theme: auto
------
theme: auto
dark:
background_color: "#111827"
content_color: "#1f2937"
---The dark: map tunes the dark variant (and implies auto on its own). Explicitly pinning theme: light or theme: dark renders static even when the rendering app passes darkTheme: the author's pinned appearance wins.
This emits color-scheme meta tags, a prefers-color-scheme: dark media query, and [data-ogsc]/[data-ogsb] selectors for Outlook.com. Client support varies: Apple Mail and Outlook honor these overrides; Gmail ignores them and applies its own automatic color inversion regardless. Themed buttons switch to the dark palette's button colors (button_color/button_text_color, and the secondary, success, danger and warning pairs); a button given its own color= keeps it in both modes, so pick one that works on light and dark backgrounds.
Dark mode is palette-based: custom per-block colors (bg=/color= on callouts and columns, divider colors) are normalized to the dark palette so text stays readable. To tune how a specific accent looks in dark mode, adjust the corresponding key in the dark: map rather than the block parameter.
The overrides target the stable emd-* classes below, which you can style yourself or reach from a custom wrapper.
CSS Hooks
Every render emits the same emd-* classes, so the css render option can reach what the theme keys don't:
| Class | On |
|---|---|
emd-root | Body |
emd-s | Every content section except raw blocks |
emd-bg | Sections painted with contentColor |
emd-top / emd-bot | First and last section of the content box |
emd-hero | Hero sections |
emd-card | Callout / column card backgrounds |
emd-hl | Highlight blocks |
emd-tbl | Tables |
emd-btn | Buttons |
emd-btn-primary, -secondary, -success, -danger, -warning, -custom | Buttons, by where their colors come from (-custom for an explicit color=) |
emd-raw | Raw HTML sections |
emd-muted | Text in muted_color: captions, header, footer |
emd-good / emd-bad | A change's text, in positive_color / negative_color |
emd-c1, emd-c2, … / emd-c1-bg, emd-c2-bg, … | Text / fill in a chart_colors entry |
The color-role classes (emd-muted, emd-good, emd-bad, emd-c<N>, emd-c<N>-bg) carry dark-mode rules, so HTML in a ::: raw block can use them to follow the dark palette too.
Rounding the Content Box
The content area is a stack of same-colored sections rather than one element, so emd-top and emd-bot mark its outer edges. They carry no styling of their own:
const { html } = await render(markdown, {
css: `
.emd-top, .emd-top > table { border-top-left-radius: 12px; border-top-right-radius: 12px; }
.emd-bot, .emd-bot > table { border-bottom-left-radius: 12px; border-bottom-right-radius: 12px; }
`,
});Use the corner longhands rather than the border-radius shorthand — a short email is a single section carrying both classes, and two competing shorthands would leave one edge square — and style the section together with its child table, since MJML paints the background on both.
::: header and ::: footer bands sit on the outer background, outside the box; a hero at the top of the document counts as the top of it. Outlook desktop renders square corners.
Web Fonts
To use a font beyond the web-safe stack (e.g. Google Fonts), pass a fonts map alongside theme. Each entry renders as an <mj-font> tag, loading the stylesheet in clients that support embedded web fonts:
const { html } = await render(markdown, {
fonts: {
Inter: 'https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap',
},
theme: {
fontFamily: 'Inter, sans-serif',
},
});The font family still has to be named in fontFamily to actually apply; fonts only ships the stylesheet link. MJML drops unused entries automatically.
The same thing works from markdown with a nested fonts: map in frontmatter. See Frontmatter → Custom Fonts.
Data Colors
Four optional keys give data emails the color roles they need. Each falls back to an existing key, so an email that leaves them unset looks as it did before, and each has a dark: override that dark mode follows.
| Key | Falls back to | Used for |
|---|---|---|
muted_color | body_color | Secondary text: stat, chart, progress, sparkline and rating labels, breakdown ranks and sub-labels, image captions, the header and footer, a change with no good or bad reading, and upcoming steps |
chart_colors | brand_color | Data colors in series order. Chart, progress, sparkline and breakdown bars default to the first, and breakdown swatches take them in order |
positive_color | success_color | Text of a change that is good news (▲ +12%) |
negative_color | danger_color | Text of a change that is bad news (▼ −3%) |
positive_color and negative_color color text on the content background, apart from the success and danger button fills, since a fill and readable text aren't always the same color.
chart_colors is a list, normally five colors:
---
theme: auto
chart_colors: ["#e76e50", "#2a9d90", "#274754", "#e8c468", "#f4a462"]
dark:
chart_colors: ["#2662d9", "#2eb88a", "#e88c30", "#af57db", "#e23670"]
---
::: chart color=chart-2
- Direct: 4,200
- Search: 3,100 {color=chart-4}
:::color=chart-2 names the palette's second color wherever a data directive takes a color (chart, progress, sparkline, stats and breakdown, on the block or a single item), and dark mode switches it to the dark palette's second color. A hex color written directly can't follow dark mode. A palette set only for light mode carries over to dark, since its colors name series.
Color Formats
Theme colors take hex, named colors, rgb(), hsl() and oklch(). Email clients don't support oklch(), and support for hsl() is uneven, so both are converted to hex, which means shadcn and Tailwind theme values can be used as they are. A color outside the sRGB range is brought in by lowering its chroma, as browsers do. A translucent color (oklch(1 0 0 / 10%)) becomes rgba().
All Theme Options
| Key | Light Default | Dark Default | Description |
|---|---|---|---|
brandColor | #18181b | #fafafa | Links, highlights, accents |
headingColor | #09090b | #fafafa | Heading text |
bodyColor | #71717a | #a1a1aa | Body text |
backgroundColor | #fafafa | #09090b | Outer background |
contentColor | #ffffff | #18181b | Content area background |
cardColor | #f4f4f5 | #27272a | Callout / code block background |
buttonColor | #18181b | #fafafa | Primary button background |
buttonTextColor | #fafafa | #18181b | Button text |
secondaryColor | #18181b | #fafafa | Secondary button border |
secondaryTextColor | #18181b | #fafafa | Secondary button text |
successColor | #16a34a | (same) | Success button background |
successTextColor | #ffffff | (same) | Success button text |
dangerColor | #dc2626 | (same) | Danger button background |
dangerTextColor | #ffffff | (same) | Danger button text |
warningColor | #d97706 | (same) | Warning button background |
warningTextColor | #ffffff | (same) | Warning button text |
dividerColor | #f4f4f5 | #27272a | Horizontal rule (---) color |
mutedColor | (bodyColor) | (bodyColor) | Secondary text: labels, captions, header and footer. See Data Colors |
chartColors | (none) | (light palette) | Data colors in series order; bars default to the first, otherwise brandColor |
positiveColor | (successColor) | (successColor) | Text of a good change |
negativeColor | (dangerColor) | (dangerColor) | Text of a bad change |
fontFamily | Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif | (same) | Font stack |
fontSize | 16px | (same) | Base font size |
lineHeight | 1.6 | (same) | Base line height |
contentWidth | 600px | (same) | Email width |
borderRadius | 8px | (same) | Border radius for buttons, callouts, highlights |
The dark theme inverts the color palette (light text on dark backgrounds) while keeping the same typography settings. You can import darkTheme directly or activate it per-email via frontmatter.