mergeDocumentDescriptors
Merges several { html, body } attribute descriptors into React props for the html and body elements: classes combined, other attributes last-wins.
import { mergeDocumentDescriptors, DocumentElementDescriptor, DocumentElementAttrs } from '@salla.sa/twilight-theme-engine/utils';In plain words
Classes and attributes on <html> and <body> (lang, dir, class="page-cart") often come from several places: the layout, the page, the theme. A descriptor is a small object saying what one of them wants: { html: { lang: 'ar' }, body: { class: 'page-cart' } }.
mergeDocumentDescriptors(a, b, …) combines any number of them into { htmlAttrs, bodyAttrs }, objects you spread onto the elements, as in <body {...bodyAttrs}>. It only calculates; it changes nothing on the page.
Signature
function mergeDocumentDescriptors(...descriptors: DocumentElementDescriptor[]): {
bodyAttrs: Record<string, string>; // React names: className
htmlAttrs: Record<string, string>;
}
interface DocumentElementDescriptor {
body?: DocumentElementAttrs;
html?: DocumentElementAttrs;
}
interface DocumentElementAttrs {
class?: string | string[]; // combined across descriptors
[attr: string]: string | string[] | undefined; // the last non-empty string wins
}Try it live
htmlAttrs = {
"lang": "en",
"dir": "rtl"
}
bodyAttrs = {
"data-page": "cart",
"className": "font-sans antialiased page-cart"
}import { mergeDocumentDescriptors } from '@salla.sa/twilight-theme-engine/utils';
const { bodyAttrs, htmlAttrs } = mergeDocumentDescriptors(
{ html: { lang: 'ar', dir: 'rtl' }, body: { class: 'font-sans antialiased' } },
{ html: { lang: 'en' }, body: { class: 'page-cart antialiased', 'data-page': 'cart' } }
);
export function Shell({ children }: { children: React.ReactNode }) {
return (
<html {...htmlAttrs}>
<body {...bodyAttrs}>{children}</body>
</html>
);
}
Example
import { HeadContent, Outlet, Scripts } from '@tanstack/react-router';
import { TwilightProvider } from '@salla.sa/twilight-theme-engine';
import { getTwilightContext } from '@salla.sa/twilight-theme-engine/tanstack';
import { mergeDocumentDescriptors } from '@salla.sa/twilight-theme-engine/utils';
function RootComponent() {
const ctx = getTwilightContext();
const { htmlAttrs, bodyAttrs } = mergeDocumentDescriptors(
{ html: { lang: ctx.locale, dir: ctx.dir }, body: { class: 'font-sans antialiased' } },
{ body: { class: 'theme-lookbook', 'data-theme': 'lookbook' } }
);
return (
<html {...htmlAttrs} suppressHydrationWarning>
<head>
<HeadContent />
</head>
<body {...bodyAttrs} suppressHydrationWarning>
<TwilightProvider>
<Outlet />
</TwilightProvider>
<Scripts />
</body>
</html>
);
}
How it behaves
classvalues from every descriptor are split on whitespace and combined without duplicates, in first-seen order. For every other attribute the last descriptor with a non-empty string wins.The output uses React names:
classbecomesclassName, and nothing else is renamed.It is
toReactProps(mergeElementAttrs(…))for each element.mergeAttrsfrom@salla.sa/twilight-theme-engine/tanstackand from/nextjsis this same function.Pure: it works in a server render and never touches the DOM. For attributes that follow a component, use
useDocumentClass(), which merges throughDocumentClassProviderand updates the real elements.
Gotchas
An empty string never clears an earlier value:
{ html: { dir: '' } }after{ html: { dir: 'rtl' } }leavesrtl. Leave the attribute out of the earlier descriptor instead.Arrays count only for
class. The type acceptsstring[]for any attribute, but on other attributes an array is dropped without a warning.TwilightProviderwriteslang,dirand theme classes to the same<html>and<body>after hydration, throughDocumentClassProvider. Its classes are added to yours, but forlanganddirits value replaces the one you rendered.docs/20-document-class.md lists
DocumentElementDescriptorandDocumentElementAttrsunder@salla.sa/twilight-theme-engine. The package root exports neither; import them from@salla.sa/twilight-theme-engine/utils.
Related
Adds classes and attributes to the page <html> and <body> while the calling component is mounted, merged with everyone else.
DocumentClassProviderCollects every useDocumentClass() request in its tree and applies the merged classes and attributes to the real body and html elements.
normalizeClasses, mergeElementAttrs, toReactPropsThe three steps of every html and body attribute merge: split class lists, merge attribute objects in order, rename class to className.