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

attrs

functionAdvancedlive demo

Turns an { html, body } attribute descriptor into React props to spread onto the html and body elements: class becomes className.

import { attrs } from '@salla.sa/twilight-theme-engine/tanstack';

In plain words

Some attributes belong on the page's outer elements: lang and dir on <html>, a page class on <body>. The engine describes them as { html: {…}, body: {…} }, with HTML attribute names such as class.

React needs className instead of class. attrs(descriptor) returns two ready objects, htmlAttrs and bodyAttrs, to spread with {...htmlAttrs}. It is a plain function: it reads nothing and changes nothing on the page.

Signature

function attrs(descriptor: DocumentElementDescriptor): {
  htmlAttrs: Record<string, string>;
  bodyAttrs: Record<string, string>;
}

// Types from @salla.sa/twilight-theme-engine/utils
interface DocumentElementDescriptor {
  html?: DocumentElementAttrs;
  body?: DocumentElementAttrs;
}
interface DocumentElementAttrs {
  class?: string | string[];
  [attribute: string]: string | string[] | undefined;
}

Try it live

One { html, body } descriptor turned into props you can spread onto <html> and <body>.Try this: repeat a class in body.class: it appears once in className. Then clear data-page: an empty value is dropped.
Storefront canvas · en · LTR
attrs(descriptor): {…} 2 keys
bodyAttrs: {…} 2 keys
data-page: "offers"
className: "page-offers sale"
htmlAttrs: {}
Controls
What a theme writes
import { attrs } from '@salla.sa/twilight-theme-engine/tanstack';

const { htmlAttrs, bodyAttrs } = attrs({
  body: { class: 'page-offers  sale sale', 'data-page': 'offers' },
});

// <html {...htmlAttrs}> and <body {...bodyAttrs}> in a root shell.

Example

app/routes/__root.tsx (excerpt)
import { HeadContent, Outlet, Scripts } from '@tanstack/react-router';
import { TwilightProvider } from '@salla.sa/twilight-theme-engine';
import { attrs, getTwilightContext } from '@salla.sa/twilight-theme-engine/tanstack';

function RootComponent() {
  const ctx = getTwilightContext();
  const { htmlAttrs, bodyAttrs } = attrs({
    html: { lang: ctx.locale, dir: ctx.dir },
    body: { class: ['theme-raed', 'antialiased'] },
  });

  return (
    <html {...htmlAttrs} suppressHydrationWarning>
      <head>
        <HeadContent />
      </head>
      <body {...bodyAttrs} suppressHydrationWarning>
        <TwilightProvider>
          <Outlet />
        </TwilightProvider>
        <Scripts />
      </body>
    </html>
  );
}

How it behaves

  • class takes a string or an array. Values are split on whitespace, repeated classes are kept once, and the result is joined with single spaces as className.

  • Every other key keeps its name and value, unless the value is an empty string or an array, which are dropped.

  • It applies the same rules as mergeAttrs(), useAttrs() and attrs from /nextjs.

Gotchas

  • Only class is renamed. Other names keep their HTML spelling, so tabindex reaches React as a lowercase prop and React warns in development. Use names that are the same in both: lang, dir, id, data-* and aria-*.

  • An empty string cannot set an attribute: { 'data-sale': '' } is dropped. Pass a value such as 'true'.

Related

Source and docs