normalizeClasses, mergeElementAttrs, toReactProps
The three steps of every html and body attribute merge: split class lists, merge attribute objects in order, rename class to className.
import { normalizeClasses, mergeElementAttrs, toReactProps } from '@salla.sa/twilight-theme-engine/utils';In plain words
Each of these small functions does one step. normalizeClasses(' a b ') turns a class string, or an array of them, into a clean list: ['a', 'b']. mergeElementAttrs([first, second]) combines attribute objects for one element: classes add up, and other attributes take the last value. toReactProps(attrs) renames class to className so the result can be spread onto a JSX element.
mergeDocumentDescriptors runs the last two for <html> and <body>. Use these when you handle a single element.
Signature
function normalizeClasses(input: string | string[] | undefined): string[] function mergeElementAttrs(descriptors: DocumentElementAttrs[]): Record<string, string> // HTML names: class, data-*, aria-*… function toReactProps(attrs: Record<string, string>): Record<string, string> // class → className
Try it live
normalizeClasses(" header is-sticky ")
→ ["header","is-sticky"]
mergeElementAttrs([...])
→ {"role":"navigation","class":"header is-sticky has-banner"}
toReactProps(merged)
→ {"role":"navigation","className":"header is-sticky has-banner"}import { mergeElementAttrs, toReactProps } from '@salla.sa/twilight-theme-engine/utils';
const attrs = toReactProps(
mergeElementAttrs([
{ class: ' header is-sticky ', role: 'banner' },
{ class: ['is-sticky', 'has-banner'], role: 'navigation' },
])
);
export function Banner() {
return <header {...attrs}>Free delivery this week</header>;
}
Example
import {
mergeElementAttrs,
toReactProps,
type DocumentElementAttrs,
} from '@salla.sa/twilight-theme-engine/utils';
export function Section({ attrs, children }: { attrs?: DocumentElementAttrs; children: React.ReactNode }) {
// Defaults first, so a caller's role replaces ours and its classes add to ours.
const props = toReactProps(mergeElementAttrs([{ class: 'section py-8', role: 'region' }, attrs ?? {}]));
return <section {...props}>{children}</section>;
}
How it behaves
normalizeClassessplits every string on whitespace and drops empty names. It keeps duplicates;mergeElementAttrsremoves them.mergeElementAttrsskipsundefinedvalues and empty strings. The mergedclassis joined with single spaces and added as the last key, and only when there is at least one class.DocumentClassProvider,useDocumentClass,attrs()from@salla.sa/twilight-theme-engine/domand the TanStack and Next.js adapters all merge withmergeElementAttrs.
Gotchas
toReactPropsrenamesclassonly.for,tabindexandhttp-equivpass through unchanged, and React logsInvalid DOM propertyfor them in development. Write the React names (htmlFor,tabIndex) in your attribute objects, or stick toclass,data-*andaria-*.Arrays count only for
class:{ role: ['banner'] }is dropped without a warning.
Related
Merges several { html, body } attribute descriptors into React props for the html and body elements: classes combined, other attributes last-wins.
syncAttrsToElementWrites an attribute object onto a real DOM element and removes what its previous call wrote, leaving attributes from other code alone.