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

syncAttrsToElement

functionAdvancedbrowserlive demo

Writes an attribute object onto a real DOM element and removes what its previous call wrote, leaving attributes from other code alone.

import { syncAttrsToElement } from '@salla.sa/twilight-theme-engine/utils';

In plain words

Adding classes to an element is easy. Removing exactly the ones you added earlier, without removing anyone else's, is not. syncAttrsToElement(element, attrs) remembers what it wrote in a data-de-managed attribute on the element; on the next call it removes those first, then writes the new set.

This is how the engine keeps the classes on <body> and <html> in step with the current page. It needs a real element, so call it in the browser, inside an effect.

Signature

function syncAttrsToElement(element: Element, next: Record<string, string>): void
// next uses HTML names (class, not className), e.g. the output of mergeElementAttrs

Try it live

syncAttrsToElement() writes attributes to a real element and remembers what it wrote in data-de-managed.Try this: press the button, then change the classes: your change replaces the old managed classes, while the class added by other code stays.
Storefront canvas · en · LTR
Runs in the browser…
Controls
Empty removes the attribute on the next sync.
What a theme writes
import { useEffect, useRef } from 'react';
import { mergeElementAttrs, syncAttrsToElement } from '@salla.sa/twilight-theme-engine/utils';

export function Promo({ children }: { children: React.ReactNode }) {
  const box = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!box.current) return;
    // HTML attribute names (class, not className), merged first.
    syncAttrsToElement(
      box.current,
      mergeElementAttrs([{ class: 'promo is-open', 'data-state': 'open' }])
    );
  }, []);

  return <div ref={box}>{children}</div>;
}

Example

app/hooks/useChatWidgetState.ts
import { useEffect } from 'react';
import { mergeElementAttrs, syncAttrsToElement } from '@salla.sa/twilight-theme-engine/utils';

/** Marks a third-party chat widget's container while the cart drawer is open. */
export function useChatWidgetState(drawerOpen: boolean) {
  useEffect(() => {
    const widget = document.getElementById('chat-widget');
    if (!widget) return;
    syncAttrsToElement(
      widget,
      mergeElementAttrs([{ class: drawerOpen ? 'is-hidden' : '', 'data-drawer': drawerOpen ? 'open' : '' }])
    );
    return () => syncAttrsToElement(widget, {});
  }, [drawerOpen]);
}

How it behaves

  • In order: read the previous record from data-de-managed (malformed JSON is ignored), remove each class and attribute it lists, add the new classes with classList.add, set the other attributes with setAttribute, then store next as the record, or remove the record when next is empty.

  • syncAttrsToElement(element, {}) removes everything the last call wrote.

  • Classes are added and removed one by one through classList, so classes nobody managed stay. Other attributes are set and removed whole.

  • Used by DocumentClassProvider, by useDocumentClass() when there is no provider, and by attrs/clearAttrs from @salla.sa/twilight-theme-engine/dom, always on document.body and document.documentElement.

Gotchas

  • Ownership is by name, not by writer: a class that other code also added is removed when it leaves the managed set. One element has one data-de-managed record, so two callers syncing the same element erase each other's attributes. TwilightProvider already syncs <body> and <html>; use useDocumentClass() there, not this.

  • It expects HTML names: { className: 'x' } sets an attribute named classname, not a class.

  • Attributes written this way are not in the server HTML; they appear after hydration.

Related

Source and docs