Branding
Your users should feel they are using your product, not ours. Three things make the editor yours:
| What | Where you set it | Why there |
|---|---|---|
| Your colours, font and corners, in light and dark mode | In your code: the appearance option (this page) | They often follow your user's light or dark setting, so your page decides |
| Your logo and product name in the editor's bar | Once, in your project's settings (Logo branding) | So no one can change them from a browser |
| "Powered by Lettrove" | Off unless you turn it on | Your product, your call |
Branding changes the editor: its bar, panels, buttons and menus. It never changes the design your user is making, because an email has to look on the canvas exactly as it will arrive in the inbox.
This page assumes you already have an editor on screen from the Quickstart or one of the framework pages (Next.js, Vue, Angular, SvelteKit, plain HTML). It only adds to that code.
Step 1: write your palette down once
Put your theme in one file and import it wherever the editor opens. Keeping it outside your
components also matters for React and Vue: an appearance that is rebuilt with new colours opens
the editor again, while a constant never does.
This is a complete theme: your light palette in variables, your dark one in darkVariables.
Replace the colours with yours.
// EditorOptions also comes from @lettrove/react and @lettrove/vue
import type { EditorOptions } from '@lettrove/embed';
export const brandAppearance = {
// 'auto' follows the device's light or dark setting;
// 'light' or 'dark' fixes one.
theme: 'auto',
// Light mode
variables: {
accent: '#5b3df5', // your brand colour
accentText: '#ffffff', // text on it (left out: black or white)
surface: '#ffffff', // top bar, block settings, menus
surfaceRaised: '#f7f7fb', // block palette, history panel
text: '#16161d', // main text
textMuted: '#5f6070', // hints, captions, counts
border: '#e3e3ea', // dividers, input outlines
focusRing: '#5b3df5', // keyboard focus (left out: accent)
radius: '10px', // corners of buttons and inputs
fontFamily: 'system-ui, sans-serif', // the editor's own text
},
// Dark mode. Backgrounds and text do not carry over from light,
// so set them here; accent, radius and font do carry over.
darkVariables: {
accent: '#8b7bff', // a lighter tint reads better on dark
surface: '#0f1117',
surfaceRaised: '#171a22',
text: '#eceef4',
textMuted: '#9aa0ad',
border: '#272b36',
},
} satisfies NonNullable<EditorOptions['appearance']>;
Those ten are every token there is. Each is optional, and one you leave out keeps Lettrove's
value. satisfies makes your code editor flag a misspelt token or a wrong value as you type. What
each token colours is in Theming and white label.
Only need your brand colour? One colour is enough to start:
import type { EditorOptions } from '@lettrove/embed';
export const brandAppearance = {
variables: { accent: '#5b3df5' },
} satisfies NonNullable<EditorOptions['appearance']>;
That one colour is used for selection, primary buttons, focus and active tabs, in light and dark mode, on Lettrove's own panels. The text on it is set to black or white for you, whichever reads better.
A value is a plain CSS value of up to 80 characters: a colour (#5b3df5, rgb(…), hsl(…),
oklch(…)), a length (10px) or a font stack. A value the editor cannot use is refused with
options_invalid, which names the token.
Step 2: give it to the editor
Pick your framework. Each tab is the editor from that framework's page with only the branding added, then the same editor following your own light / dark switch. Your choice is remembered across these docs.
- Next.js
- React
- Vue
- Angular
- SvelteKit
- HTML
The editor runs in the browser, so it lives in a client component, exactly as on the
Next.js page. Put lettrove-theme.ts beside it:
'use client';
import { LettroveEditor } from '@lettrove/react';
import { brandAppearance } from './lettrove-theme';
async function getToken() {
const response = await fetch('/lettrove-token', { method: 'POST' });
return (await response.json()).token;
}
export function EmailEditor() {
return (
<LettroveEditor
publishableKey={process.env.NEXT_PUBLIC_LETTROVE_PUBLISHABLE_KEY!}
getToken={getToken}
appearance={brandAppearance}
style={{ height: '100vh' }}
/>
);
}
Following your theme switch, with next-themes. Hide the editor's own switch and pass your
theme in; a new appearance.theme switches the open editor at once, without reopening it:
'use client';
import { useMemo } from 'react';
import { useTheme } from 'next-themes';
import { LettroveEditor } from '@lettrove/react';
import { brandAppearance } from './lettrove-theme';
async function getToken() {
const response = await fetch('/lettrove-token', { method: 'POST' });
return (await response.json()).token;
}
export function EmailEditor() {
// 'light' or 'dark'; undefined until next-themes knows
const { resolvedTheme } = useTheme();
const appearance = useMemo(() => {
const known = resolvedTheme === 'dark' || resolvedTheme === 'light';
const theme = known ? resolvedTheme : 'auto';
return { ...brandAppearance, theme } as const;
}, [resolvedTheme]);
return (
<LettroveEditor
publishableKey={process.env.NEXT_PUBLIC_LETTROVE_PUBLISHABLE_KEY!}
getToken={getToken}
appearance={appearance}
features={{ themeSwitch: false }} // your switch, not the editor's
style={{ height: '100vh' }}
/>
);
}
import { LettroveEditor } from '@lettrove/react';
import { brandAppearance } from './lettrove-theme';
async function getToken() {
const response = await fetch('/lettrove-token', { method: 'POST' });
return (await response.json()).token;
}
export function Editor() {
return (
<LettroveEditor
publishableKey="lt_pk_test_paste_yours_here"
getToken={getToken}
appearance={brandAppearance}
style={{ height: '100vh' }}
/>
);
}
Following your own theme switch. Hide the editor's switch and pass your theme in; a new
appearance.theme switches the open editor at once, without reopening it:
import { useMemo } from 'react';
import { LettroveEditor } from '@lettrove/react';
import { brandAppearance } from './lettrove-theme';
async function getToken() {
const response = await fetch('/lettrove-token', { method: 'POST' });
return (await response.json()).token;
}
export function Editor({ theme }: { theme: 'light' | 'dark' }) {
// Memoised: the colours stay the same object; only the theme changes.
const appearance = useMemo(() => ({ ...brandAppearance, theme }), [theme]);
return (
<LettroveEditor
publishableKey="lt_pk_test_paste_yours_here"
getToken={getToken}
appearance={appearance}
features={{ themeSwitch: false }} // your switch, not the editor's
style={{ height: '100vh' }}
/>
);
}
Keeping the editor's own switch instead? onThemeChanged={({ theme }) => …} tells you when your
user changes it ('light' | 'dark' | 'auto'), so you can remember it and pass it back as
appearance.theme next time.
<script setup lang="ts">
import { LettroveEditor } from '@lettrove/vue';
import { brandAppearance } from './lettrove-theme';
async function getToken() {
const response = await fetch('/lettrove-token', { method: 'POST' });
return (await response.json()).token;
}
</script>
<template>
<LettroveEditor
publishable-key="lt_pk_test_paste_yours_here"
:get-token="getToken"
:appearance="brandAppearance"
style="height: 100vh"
/>
</template>
Following your own theme switch. A computed keeps the colours the same and changes only the
theme, which switches the open editor without reopening it:
<script setup lang="ts">
import { computed, ref } from 'vue';
import { LettroveEditor } from '@lettrove/vue';
import { brandAppearance } from './lettrove-theme';
const theme = ref<'light' | 'dark'>('light'); // your product's own theme
const appearance = computed(() => ({ ...brandAppearance, theme: theme.value }));
async function getToken() {
const response = await fetch('/lettrove-token', { method: 'POST' });
return (await response.json()).token;
}
</script>
<template>
<button @click="theme = theme === 'light' ? 'dark' : 'light'">
Switch theme
</button>
<LettroveEditor
publishable-key="lt_pk_test_paste_yours_here"
:get-token="getToken"
:appearance="appearance"
:features="{ themeSwitch: false }"
style="height: 100vh"
/>
</template>
Keeping the editor's own switch? @theme-changed="({ theme }) => …" tells you when your user
changes it.
import {
AfterViewInit, Component, ElementRef, OnDestroy, viewChild,
} from '@angular/core';
import { createEditor, type EditorHandle } from '@lettrove/embed';
import { brandAppearance } from './lettrove-theme';
async function getToken() {
const response = await fetch('/lettrove-token', { method: 'POST' });
return (await response.json()).token;
}
@Component({
selector: 'app-root',
template: '<div #editor style="height: 100vh"></div>',
})
export class App implements AfterViewInit, OnDestroy {
private readonly container =
viewChild.required<ElementRef<HTMLDivElement>>('editor');
private editor: EditorHandle | null = null;
async ngAfterViewInit() {
this.editor = await createEditor({
container: this.container().nativeElement,
publishableKey: 'lt_pk_test_paste_yours_here',
getToken,
appearance: brandAppearance,
features: { themeSwitch: false }, // if you call setTheme from yours
});
}
/** Call from your own theme switch; the open editor changes at once. */
async setTheme(theme: 'light' | 'dark' | 'auto') {
// Resolves to the theme applied: 'light' or 'dark'.
await this.editor?.setTheme(theme);
}
ngOnDestroy() {
this.editor?.destroy();
}
}
No theme switch of your own? Leave out features and setTheme: the editor keeps its own switch,
and theme: 'auto' follows the device.
lettrove-theme.ts lives in src/lib/:
<script lang="ts">
import { onMount } from 'svelte';
import { createEditor, type EditorHandle } from '@lettrove/embed';
import { brandAppearance } from '$lib/lettrove-theme';
let container: HTMLDivElement;
let editor: EditorHandle | null = null;
async function getToken() {
const response = await fetch('/lettrove-token', { method: 'POST' });
return (await response.json()).token;
}
onMount(() => {
createEditor({
container,
publishableKey: 'lt_pk_test_paste_yours_here',
getToken,
appearance: brandAppearance,
}).then((opened) => (editor = opened));
return () => editor?.destroy();
});
</script>
<div bind:this={container} style="height: 100vh"></div>
Following your own theme switch: call editor?.setTheme('dark') (or 'light', 'auto') when
yours changes; the open editor switches at once. Add features: { themeSwitch: false } to
createEditor so there is one switch, not two.
No build step: the palette goes straight into createEditor.
<div id="editor" style="height: 100vh"></div>
<script src="https://cdn.jsdelivr.net/npm/@lettrove/embed@1/dist/lettrove.iife.js"></script>
<script>
async function getToken() {
const response = await fetch('/lettrove-token', { method: 'POST' });
return (await response.json()).token;
}
Lettrove.createEditor({
container: '#editor',
publishableKey: 'lt_pk_test_paste_yours_here',
getToken,
appearance: {
theme: 'auto',
variables: { accent: '#5b3df5', radius: '10px' },
darkVariables: { accent: '#8b7bff' },
},
}).then((editor) => {
// Your own theme switch, if you have one:
// document.querySelector('#dark').onclick = () => editor.setTheme('dark');
});
</script>
Step 3: use the colours you already have
Your product's colours usually live in CSS variables already (Tailwind, shadcn/ui, Material, Bootstrap…). The editor runs on its own site, in a frame, so it cannot read your page's CSS: read the values in your page and pass them in. A small helper does it once:
import type { EditorOptions } from '@lettrove/embed';
/** A CSS variable from your page: `--primary` → `#5b3df5`. */
function cssVar(name: string): string | undefined {
const root = getComputedStyle(document.documentElement);
const value = root.getPropertyValue(name).trim();
if (!value) return undefined;
// shadcn/ui stores bare HSL numbers ("262 83% 58%"): wrap them.
return /^[\d.]+\s+[\d.]+%\s+[\d.]+%$/.test(value) ? `hsl(${value})` : value;
}
type Appearance = NonNullable<EditorOptions['appearance']>;
/** Read in the browser, after your styles have loaded (not on the server). */
export function appearanceFromYourCss(): Appearance {
const variables = {
accent: cssVar('--primary'),
accentText: cssVar('--primary-foreground'),
surface: cssVar('--background'),
surfaceRaised: cssVar('--muted'),
text: cssVar('--foreground'),
textMuted: cssVar('--muted-foreground'),
border: cssVar('--border'),
radius: cssVar('--radius'),
};
// Leave out what your CSS does not define: the editor keeps its own.
const defined = Object.entries(variables).filter(([, v]) => v);
return { variables: Object.fromEntries(defined) };
}
Rename the variables to yours. Read them once, when the editor opens: colours that change while it
is open (a new brand colour) take effect the next time it opens. Light and dark switch live through
theme, as above. If your dark palette lives under a .dark class, read it with that class on and
pass it as darkVariables.
Step 4: your logo and product name
Your logo and product name in the editor's bar are set once, in your project's settings at lettrove.com, with no code: they apply to every editor in the project, in every framework. A page cannot set or remove them, so no one can put another company's name on your editor from a browser. Logo branding has the steps, what a logo needs, and how it looks in light and dark mode.
Step 5: "Powered by Lettrove"
Off by default: your users see your brand only. To show it, pass branding: { poweredBy: true }
(in React and Vue, the branding prop). The page can turn it on, never off.
Check it before you ship
- Open the editor in light and dark mode (your switch, or your device's setting with
theme: 'auto'). Selected blocks, primary buttons and focus outlines are in your colour in both. - Press Tab through the editor's bar: the focus outline is visible on your surfaces.
- Your logo reads on both backgrounds, and the bar shows your product name.
- With a test key, a wrong token or value stops the editor and says which. Fix it before you switch to a live key, which drops a wrong value with a warning in the console instead.
For your AI assistant
If an assistant is wiring this up for you, these are the rules it needs:
- Colours, font and corners go in
appearance(theme,variables,darkVariables,panels). The tokens areaccent,accentText,surface,surfaceRaised,text,textMuted,border,focusRing,radiusandfontFamily, and there are no others. The editor's markup cannot be styled with CSS. - The logo and product name are set in Settings → Embed at lettrove.com, never in code.
- Keep
appearancea constant (or memoised ontheme) in React and Vue; new colours reopen the editor, a newthemeswitches it live. With your own switch, setfeatures: { themeSwitch: false }and passappearance.theme(React, Vue) or calleditor.setTheme()(anything else). - Fonts load inside the editor's frame, so name a font the user's device has or a system stack; a font loaded on your page is not available there.
- Branding never changes the design being edited.
The full reference is Theming and white label, and every option is in the
createEditor reference.