The editor is two surfaces with two different loyalties, and the theming story is understanding which is which.
The chrome follows your tokens
Everything around the email (the block toolbar, the settings sidebar, the
add-block menu, the selection ring, the bottom sheet on small screens) is
built from @voila.dev/ui components and styled with the same token classes
as the rest of the system: --ring for the selection ring, --border for the
dashed drop slots, and so on through the sidebar's inputs and buttons.
There is nothing to configure. If your app imports any
@voila.dev/ui/styles/themes/*.css (or defines the same CSS custom properties
itself), the editor chrome picks your brand up automatically, dark mode
included. Change --primary and the sidebar's controls follow; toggle the
theme and the toolbar toggles with it.
The card follows your email
The email card itself — the grey backdrop, the white card, the brand colour of headings and buttons — is the preview, and what it should match is your renderer, not your app. An email client has no CSS custom properties and no dark mode toggle, so a canvas that restyled itself with the app would stop being an honest preview of what the recipient receives.
That is what the theme prop is for:
<EmailBlockEditor
blocks={BLOCKS}
document={document}
onDocumentChange={setDocument}
theme={{
color: { brand: "#151b77", canvas: "#f4f4f7" },
locale: "fr-FR",
}}
/>It is merged over the defaults section by section, so { color: { brand } }
is a complete theme: the other colours, the font and the metrics keep theirs.
Two editors in the same app can therefore look different, which matters the
moment you preview two brands, or a transactional template beside a marketing
one.
The defaults point at the kit's tokens (var(--color-primary) and friends), so
an editor with no theme follows the app — the right behaviour for a demo and
the wrong one for a real email. Point it at your renderer's literal palette and
the canvas stops lying.
The theme covers the colours, the font stack, the preview locale, the two heading sizes, the grid gutter, the image width ratios and the two preview widths. The whole default is exported if you want to read it or build on it:
import {
DEFAULT_EMAIL_EDITOR_THEME,
mergeEmailEditorTheme,
} from "@voila.dev/ui/email-block-editor";mergeEmailEditorTheme is the same merge the editor does, for when you need
the resolved object outside one — a preview thumbnail, say.
Locale
theme.locale (en-US by default) is the locale the canvas previews prices
and dates in. Deliberately not the browser's: the sent email formats per
recipient, and an author should see what a recipient gets rather than what
their own machine happens to be configured for.
Headers and footers
The header and footer placeholders are neutral stand-ins for the branded chrome your renderer adds at send time. Replace them to preview the real thing:
<EmailBlockEditor
blocks={BLOCKS}
document={document}
onDocumentChange={setDocument}
headerSlot={<BrandHeader />}
footerSlot={<BrandFooter />}
/>Composing the parts yourself, the same two are EmailEditor.CardHeader and
EmailEditor.CardFooter, each taking a render prop. The footer sits outside
the card on purpose: that is where a sent email puts contact details and the
unsubscribe line.
Copy
Colours are not the only thing a host owns. Every string the editor shows
comes from labels, merged the same way:
<EmailBlockEditor
blocks={BLOCKS}
document={document}
onDocumentChange={setDocument}
labels={{ chrome: { addBlock: "Ajouter un bloc" } }}
/>There is no i18n runtime in the package: your app has one already, and a
second would fight it. The strings are data, in four sections — chrome,
blockNames, fields, blocks — and a label that reads an index is a
function, because Item 3 and 3e élément do not share a word order.