hiliteColor leaves transparent noise behind, and nobody says so
hiliteColor leaves transparent noise behind, and nobody says so
TLDR: execCommand('hiliteColor', 'transparent') is the standard way to clear a highlight, and in Chromium it works, but it leaves every affected span carrying an inline background-color: transparent. That noise survives export and pollutes the document the way a real highlight would. The fix is a cleanup pass over the selection after the command.
The bug class
Clearing a marker via execCommand produces output that looks correct in the browser and is wrong in the serialized document. This is a whole class: execCommand commands mutate the DOM in browser-specific ways, and the editor shows the mutation but never shows the leftover markup.
Why the API produces it
hiliteColor with a color value sets background-color on the selection, emitting an inline style when styleWithCSS is true. To remove the highlight you pass the color transparent. The browser obediently sets background-color: transparent on every affected span.
Semantically that is correct. Visually it is identical to no highlight. But structurally it is a property that says "I have a background and it is nothing", and downstream consumers disagree about what that means:
<span style="background-color: transparent">clean text</span>
A serialized copy carries that style. A diff tool counts it as a formatting change. A document IR that captures inline styles stores a background on a run that never had one. Nothing renders differently, so the bug is invisible until some consumer decides what "transparent" means.
The fix with explanation
After the command, walk the affected subtree and remove exactly the style property that the command added, then drop the style attribute entirely when it empties:
scope.querySelectorAll('[style]').forEach(el => {
const bg = el.style.backgroundColor || '';
if (/^(transparent|rgba?\(0,0,0,0\))$/i.test(bg)) {
el.style.removeProperty('background-color');
}
if (!el.getAttribute('style')) el.removeAttribute('style');
});
The scope is the selection's editable surface, so the cleanup never strays outside the region the user touched. The empty style attribute is removed too, otherwise the span still carries a marker that means nothing.
How to prevent the class
Three rules cover most of it. First, always run a cleanup pass after execCommand calls that take a color value, and unit-assert the DOM afterward rather than only screenshotting it. Second, pick removal semantics that do not involve writing a special color: if the command cannot truly unset, remove the property directly. Third, whenever a browser command is documented as "deprecated, do not rely on it", assume its output shape is a lie you have to correct, because nobody is fixing the quirks anymore.
The generalizable lesson
An edit that is visually correct and structurally wrong is the hardest defect to catch, because every human check passes. When you mutate the DOM through an API whose contract is "it changes, trust us", verify the serialized bytes, not the pixels.