Browser Translation
A reader who runs Google Translate on your page gets a rewritten DOM. That breaks two things.
The content goes wrong. A translated CLI flag, env var, shortcut key, or one-time passcode names something that does not exist, and the reader copies it anyway.
The render goes wrong. The engine replaces each text node with a <font> wrapper and reparents the original node: React's reference to it stays alive, but its parent is now the <font>. The next update that names that node (an icon inserting before it, or the node itself unmounting) asks the DOM to touch a child its parent no longer owns. The DOM raises NotFoundError, React re-throws the raw DOMException, and with no error boundary above it the React root tears down. The page goes blank.
This is not hypothetical. It shipped in mantle 0.83.2: the first click of any submit button blanked a translated page, because isLoading synthesized an icon beside bare text children.
What survives the rewrite and what does not
The failure needs React to insert before, or remove, a text node the engine reparented. Element children are never reparented.
The last row is the one to design toward, and it is narrower than it looks. When a host element's children is one string, one number, or one bigint, React writes it through textContent, which wipes the <font> wrapper and repairs the subtree instead of fighting it. A component element that returns bare text at its root does not qualify. That component owns no host node, so React mounts and removes the text node by itself.
The reorder row is about the reference node, not the moved one. React picks that node by walking to the next host sibling. The walk returns a text node as readily as an element, so a keyed list that re-sorts in front of a bare text summary throws.
The unmount row is about the outermost host node. A portal, a Fragment, an array, and a component that returns bare text own none. React removes each of their host children one at a time, so a bare text child among them throws.
What mantle guarantees
Each bullet is one cure. A part picks the cure its own shape needs.
- A label slot wraps the text. The default, and what most parts do.
Button,Badge, andAnchorrenderchildreninside one, so an appearingiconorisLoadingspinner inserts before an element sibling.Select.Itemwrapschildren, andSelect.Valuewraps itsplaceholderand your ownchildrenthe same way. So doSelect.Trigger,Tooltip.Content,Tabs.Triggeron both paths,Toast.Root,MultiSelect.Item,DropdownMenu.SubTrigger,DropdownMenu.CheckboxItem,DropdownMenu.RadioItem,DataTable.ActionCell,DataTable.ActionHeader,SelectableList.SelectAll,SelectableList.Empty,AlertCenter.Item, and each chart'sTooltipandLegendrender prop. Every slot butAnchor's isdisplay: contents, so it adds no box and changes no layout. - A wrapper that never unmounts holds a conditional sibling.
Sidebar.TriggerandSidebar.Tooltipalways render the flex row that pairs a row's label with its chord chips, so ashortcutthat resolves after the first paint appends beside an element.Sidebar.SearchTriggeralways renders its chord span on the defaultbuttonpath and hides it withdisplay: nonewhile it is empty. - A decorative sibling renders after the text.
DataTable.ActionHeaderrenders its sticky indicator last, which turns the mount into an append. The move cures that mount and nothing else, so the part carries a label slot too. - An announcer stays mounted and takes a lone string.
DataTable.HeaderSortButtonrenders its label throughButton's label slot, and the sort state lives in attributes (aria-sorton the header cell,data-sort-directionon the button), so a sort change inserts no node before text. - Untranslatable content carries
translate="no".Code,Kbd,TruncatedText,CodeBlock.Code,CodeBlock.Title,Command.Shortcut,DropdownMenu.Shortcut, andOtpInput.Slotmark their content, so the engine skips those subtrees entirely. The attribute classifies content and is never a part's fix for a crash. The attribute rides on aSelect.Item's portaled label too, so an ID or a path stays untranslated in the trigger. It rides on the two chord slots too:sidebar-tooltip-shortcutinSidebar.TriggerandSidebar.Tooltip, andsidebar-search-trigger-shortcutinSidebar.SearchTrigger. A chord passed as a raw<kbd>or a bare string keeps its key name there.
Each label slot is public API. Its name is the part's own data-slot plus -label, and the part's docs page lists it.
One caveat, and it is the sentence to act on: when a mantle part renders anything beside your children, your children are not a lone child, and the self-healing row does not apply to them. That is why these parts wrap, and it is the rule your own components need too.
Breadcrumb is the exception that cannot wrap: Breadcrumb.List renders an <ol>, so a wrapper would nest an <li> in an <li>. A content crumb must render a host element at its root instead.
A label slot protects the insert mantle makes. It cannot protect an insert your own children make inside it; that is the next section.
Who owns what
- Mantle owns its parts. Every part stays safe on a translated page with plain
children, a bare string included. A part that renders anything beside your children wraps them in a label slot, so the node React removes is an element. A crash inside a part's own markup is a mantle bug: report it, and do not work around it withtranslate="no". - You own your own markup. The rules below cover the shapes a component of yours can emit. A label slot cannot protect a conditional element you render before bare text inside it.
- Both keep
translate="no"to content. Set it on a string a reader copies or retypes, through the part that owns it when one exists. Never set it to stop a crash, and never set it on prose. A translated page that stays readable is the goal, and the attribute takes a string out of that page, so every use has to earn it.
The rules for your own code
Never render a conditional element immediately before bare text children.
Three fixes work. Pick by what the conditional element is:
- Wrap the text in an element. The default answer, and what mantle's own parts do.
- Move the element after the text. A mount then becomes an append, which is safe. Prefer this when the element is decorative and out of flow, because it adds no DOM.
- Mount the element unconditionally and write text into it. A
textContentwrite is safe and self-healing. Prefer this for asr-onlyannouncer, which costs no layout.
Two more shapes throw with no conditional element in sight.
Never re-sort a keyed list in front of bare text. React inserts each moved element before the next host sibling. That sibling can be the text node. Wrap the text, and the moved element then names an element.
Never leave bare text as a direct child of a portal, a Fragment, or an array. None of the three owns a host node, and neither does a component that returns bare text at its root. React removes each child by itself, so a bare text child is the outermost node.
A child list that never changes is safe, however long it is. Every shape above needs an update that moves, removes, or mounts a node beside text that stays put.
Buttons with an icon, or two
Pass a button's icon through icon, never as a child. The slot sizes the glyph, holds it at shrink-0, tightens the padding on its side, and hands its place to the spinner when isLoading turns on.
A button can still carry a second icon. Put it after the label text as a child: the icon slot and every child of the label slot are flex items of the button, so the button's gap falls between all of them.
The label text is wrapped, which matters when that second icon is conditional: a trailing icon appends and is safe either way, but a leading child icon inserts before the text, and the wrapper is what keeps it from throwing.
Marking content untranslatable
Set translate="no" on the element that holds a string a reader copies or retypes: code, a CLI flag, an env var, a YAML key, a shortcut key, a filename, an ID, a passcode. Reach for the part that already carries it before a raw attribute: Code, Kbd, CodeBlock.Code, CodeBlock.Title, Command.Shortcut, DropdownMenu.Shortcut, and OtpInput.Slot.
The attribute classifies content. It is not a fix for a crash. Descendants inherit it, so the engine skips the subtree and a crash shape under it stops throwing, but the shape stays, and the next prose child hits it. When one of the rules above throws, fix the structure, and set translate="no" only when the content also belongs on the list. A search query, a name, a status label, and any other prose stay translatable.
Code and CodeBlock.Title set the attribute as an overridable default, because both also hold prose. Pass translate="yes" there when the content is a word rather than a token. Kbd, CodeBlock.Code, Command.Shortcut, DropdownMenu.Shortcut, and OtpInput.Slot lock it: those omit the prop from their types, so no call site can turn the guard off.
A label that has to shrink
The label slot takes no styling of its own: display: contents generates no box, so there is nothing there to size. Style your own child instead, which is still a flex item of the button:
This is the shape a wrapper box breaks. flex-1 and min-w-0 need the span to be a flex item of the button, so a label slot that laid out a box would drop both and the text would push past the button's width instead of clamping.
Testing it
Reproduce what the engine does, then run the update that used to throw. Wrapping the text nodes is the whole model:
Rejecting a translate="no" subtree is what makes that attribute testable: a component that stamps it survives a mutation that would otherwise detach the text nodes React holds. happy-dom raises the same NotFoundError as Chromium here, so these tests need no real browser.
Limits
The model is a reparent, which is what Google Translate does. An engine that instead detaches and replaces the text node produces a different symptom. Safari and Firefox translation are unverified.
Nothing here patches the DOM. A Node.prototype.removeChild fallback would make arbitrary product code safe, and mantle does not install one: the guarantees above are structural, per component.