Skip to content
Twilight React Playground
ثيم رائدaren

useDocumentClass

hookAdvancedserverbrowserlive demo

Adds classes and attributes to the page <html> and <body> while the calling component is mounted, merged with everyone else.

import { useDocumentClass } from '@salla.sa/twilight-theme-engine/hooks/useDocumentClass';

In plain words

A page component normally draws inside the page, not on <body> or <html>. With useDocumentClass({ body: { class: 'page-cart' } }), the body gets that class while your component is on screen, and loses it when the component is removed.

Several components can do this at once. Their classes are combined; for any other attribute, the most recent one wins.

Signature

function useDocumentClass(descriptor: DocumentElementDescriptor): void

// DocumentElementDescriptor and DocumentElementAttrs are exported from
// @salla.sa/twilight-theme-engine/utils
interface DocumentElementDescriptor {
  body?: DocumentElementAttrs;
  html?: DocumentElementAttrs;
}
interface DocumentElementAttrs {
  class?: string | string[];                        // merged across components
  [attr: string]: string | string[] | undefined;    // most recent wins
}

Try it live

The hidden component adds a class and a data attribute to this page's <body> while it is mounted. The engine's own body classes stay.Try this: read the body, switch Mounted off, and read it again: your class and attribute are gone, while the engine’s color-mode-… class remains.
Storefront canvas · ar · RTL
Runs in the browser…
Controls
Mounted
What a theme writes
import { useDocumentClass } from '@salla.sa/twilight-theme-engine/hooks/useDocumentClass';

export function CompactLayout({ children }: { children: React.ReactNode }) {
  useDocumentClass({
    body: { class: 'playground-compact', 'data-playground-density': 'compact' },
  });
  return <>{children}</>;
}

Example

app/components/layout/CheckoutShell.tsx
import { useDocumentClass } from '@salla.sa/twilight-theme-engine/hooks/useDocumentClass';

export function CheckoutShell({ children }: { children: React.ReactNode }) {
  useDocumentClass({
    body: { class: ['page-checkout', 'no-sticky-header'], 'data-flow': 'checkout' },
  });
  return <main className="checkout-shell">{children}</main>;
}

How it behaves

  • Under TwilightProvider (which renders DocumentClassProvider) each call registers into one store, keyed by useId(), and a single sync component writes the merged result to document.body and document.documentElement.

  • class values are union-merged and de-duplicated. Any other attribute takes the value of the most recent registration; a registration whose content changes is removed and added again, so it becomes the most recent.

  • The engine itself registers html: { lang, dir } and the theme's body classes (color-mode-…, footer-is-dark…) this way, from ThemeDocumentSync.

  • It re-registers only when JSON.stringify(descriptor) changes, so an inline object literal is fine.

  • Without any provider it writes to the DOM directly and records what it set in a data-de-managed attribute.

Gotchas

  • It registers in an effect, so the attributes are not in the server HTML. CSS that depends on them (above-the-fold layout, hiding a header) flashes after hydration. Put attributes that must be there from the first paint on <html>/<body> in your root route instead.

  • Setting lang or dir competes with the engine's own registration of those attributes; whichever registered last wins, and a later change of language re-registers the engine's.

  • docs/20-document-class.md lists DocumentElementDescriptor on the package root. Import it from @salla.sa/twilight-theme-engine/utils.

Related

Source and docs