Theme workbench: recoloring the widget via the customization API
The messaging widget doesn’t have to look off-the-shelf. The customization API recolors every element at runtime, from the header to the chat bubble:
zE('messenger:set', 'customization', {
theme: {
primary: '#f0b90a',
onPrimary: '#1e1e20',
background: '#f2eee6',
onBackground: '#1e1e20',
// … 17 properties in total
},
});
The 17 theme properties
Every surface has a partner color with an on… prefix for the text and icons on it:
primary/onPrimary— header and main elementsbackground/onBackground— widget backgroundmessage/onMessage— the customer’s chat bubblebusinessMessage/onBusinessMessage— replies from bot and teamaction/onAction/onSecondaryAction— buttonsconversationListBackground/onConversationListBackground— conversation listnotify/onNotifyanderror/onError— system messages
Anything you don’t set falls back to the defaults from the Admin Center. A partial theme of four lines is enough for a branded look. Every value is a plain CSS color, and the property names are case-sensitive.
Picking contrast automatically
The workbench picks the on… colors for you: compute the relative luminance per WCAG, then the variant with the higher contrast wins. The core stays small:
function luminance(hex) {
const channel = (i) => {
const c = parseInt(hex.slice(i, i + 2), 16) / 255;
return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
};
return 0.2126 * channel(1) + 0.7152 * channel(3) + 0.0722 * channel(5);
}
function contrast(a, b) {
const [light, dark] = [luminance(a), luminance(b)].sort((x, y) => y - x);
return (light + 0.05) / (dark + 0.05);
}
const textColor = contrast(background, '#1e1e20') >= contrast(background, '#f2eee6')
? '#1e1e20'
: '#f2eee6';
So nobody can click together an unreadable widget, whatever base color they pick.
Gotcha 1: the dead zE reference
The Zendesk snippet gives you a window.zE right away, but that’s only a queue. After boot, the widget replaces that function with the real API. If you stashed zE in a variable early, your later calls hit nothing: no error, no warning, no effect.
// ❌ the reference goes stale after boot:
const zE = window.zE;
later(() => zE('messenger:set', 'customization', …)); // fizzles out
// ✅ resolve window.zE fresh on every call:
const ze = (...args) => window.zE?.(...args);
Gotcha 2: contentScale is in the docs and does nothing
The docs describe a contentScale option (50–200%). As of July 2026, the shipped widget ignores it completely: as a factor, as a percentage, set live or before render, no effect, no error. That’s why the workbench has no scaling slider. If Zendesk ever wires the option up, it comes back.
Bonus for the record: a second render call duplicates the widget inside the container. There’s no un-render: changing render options means reloading the page.