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

TwilightProvider

providerBeginnerserverbrowserlive demo

The component a theme mounts once around its pages; it shares the store, theme, language and navigation with everything inside it.

import { TwilightProvider, TwilightProviderProps, TwilightConfig } from '@salla.sa/twilight-theme-engine';

In plain words

A component is a function that returns what should appear on the page. A provider is a component that wraps other components and makes data available to all of them, however deep they sit.

TwilightProvider is the provider every theme mounts exactly once, in app/routes/__root.tsx, around <Outlet /> (the spot where the current page appears). Everything else in the engine reads from it: the store, the merchant's theme settings, translations, links and navigation, toast messages and hook slots.

You rarely pass it anything. The reference theme passes only translations; the other props switch built-in behaviour off or change it.

Signature

function TwilightProvider(props: TwilightProviderProps): React.ReactElement

interface TwilightProviderProps {
  children: ReactNode;
  translations?: TranslationMessages;           // from 'virtual:twilight/theme-translations'
  layout?: ComponentType<LayoutProps> | false;  // default: MasterLayout
  client?: { framework?: 'tanstack' | 'nextjs'; interceptLinks?: boolean } | false;
                                                // default: { framework: 'tanstack', interceptLinks: true }
  toast?: boolean;                              // default: true
  toastMobilePosition?: ToastPosition;          // default: 'bottom-center'
  addToCartToast?: boolean;                     // default: true
  routeClass?: boolean;                         // default: true
  skeleton?: ReactNode;                         // shown while isReady is false
  onReady?: (data: { store: Store; theme: Theme }) => void;
  debug?: boolean;                              // default: false
  onError?: (error: Error) => void;             // accepted, never called
  gtm?: boolean;                                // accepted, never read
}

interface TwilightConfig {
  debug?: boolean;
}

Try it live

A readout of the one TwilightProvider this page is rendered inside. A theme mounts it once, so this demo reads it instead of mounting another.Try this: switch the language pill (top bar, or the ☰ menu on a phone) and watch locale, dir and the rtl/ltr body class change.
Storefront canvas · en · LTR
Runs in the browser…
What a theme writes
// app/routes/__root.tsx
import { HeadContent, Outlet, Scripts } from '@tanstack/react-router';
import { TwilightProvider } from '@salla.sa/twilight-theme-engine';
import {
  createTwilightRootRoute,
  getTwilightContext,
} from '@salla.sa/twilight-theme-engine/tanstack';
import themeTranslations from 'virtual:twilight/theme-translations';

export const Route = createTwilightRootRoute()({ shellComponent: RootComponent });

function RootComponent() {
  const ctx = getTwilightContext();
  return (
    <html lang={ctx.locale} dir={ctx.dir} suppressHydrationWarning>
      <head>
        <HeadContent />
      </head>
      <body suppressHydrationWarning>
        <TwilightProvider translations={themeTranslations}>
          <Outlet />
        </TwilightProvider>
        <Scripts />
      </body>
    </html>
  );
}

Example

app/routes/__root.tsx
import { HeadContent, Outlet, Scripts } from '@tanstack/react-router';
import { TwilightProvider } from '@salla.sa/twilight-theme-engine';
import {
  createTwilightRootRoute,
  getTwilightContext,
} from '@salla.sa/twilight-theme-engine/tanstack';
import themeTranslations from 'virtual:twilight/theme-translations';

export const Route = createTwilightRootRoute()({ shellComponent: RootComponent });

function RootComponent() {
  const ctx = getTwilightContext();
  return (
    <html lang={ctx.locale} dir={ctx.dir} suppressHydrationWarning>
      <head>
        <HeadContent />
      </head>
      <body suppressHydrationWarning>
        <TwilightProvider translations={themeTranslations}>
          <Outlet />
        </TwilightProvider>
        <Scripts />
      </body>
    </html>
  );
}

How it behaves

  • What it renders, outermost first: DocumentClassProvider, the twilight context, I18nProvider, the theme and route body-class syncs; then, once isReady, the framework navigation setup (NavigationProvider + LinkProvider) around WidgetHead, AppsSnippets, HookSlot body:start, Toaster, NavigationInterceptor, your layout with the page inside, and HookSlot body:end.

  • Store data never arrives through props. twilightMiddleware() (in app/start.ts) and createTwilightRootRoute() load the settings for the request, and the provider reads them from getTwilightContext() on its first render. With settings present, isReady is already true in the server render.

  • With no settings, isReady starts false and the provider renders only <div className="loading-overlay"> holding skeleton (or a .loading-spinner) until Salla.onReady() resolves.

  • In the browser it calls Salla.init() with the settings and the page, awaits Salla.onReady(), dispatches a theme::ready event on document, then calls onReady({ store, theme }). This runs again only if the initial locale changes.

  • Body classes it keeps in sync: salla-<theme name>, color-mode-light or color-mode-dark, rtl or ltr, preview-mode, font-<name>, footer-is-dark or footer-is-light, topnav-is-dark, is-sticky-product-bar, plus lang and dir on <html>. With routeClass, a per-page class is added too, such as cart or product-single.

  • It follows the Salla SDK login and logout events, keeps useTwilight().authToken current, and mirrors a fresh token into the token cookie (30 days, SameSite=Lax) so the next server render is signed in.

  • client.framework picks a code-split navigation setup (TanStack by default), and routeDocumentSyncs[framework] the route class sync. client={false} mounts neither.

  • The same component is also exported from @salla.sa/twilight-theme-engine/providers.

Gotchas

  • Mount it once. A second provider brings a second DocumentClassProvider; both write to the same <body> and share its data-de-managed record, so each removes the classes the other added (syncAttrsToElement in src/utils/document-class.ts), and the SDK would be initialised twice. Put per-section wrappers in layout or in your routes instead.

  • onError, gtm and debug do nothing you can see. onError is handed on as _onError and never called (src/providers/twilight-init.ts); gtm is never read, because Google Tag Manager is injected by a default body:start hook handler; debug only appears as useTwilight().config.debug. docs/02-theme-engine-core.md calls debug verbose engine logging.

  • Nothing inside skeleton can use the engine Link or a working useNavigate(): the overlay renders before the navigation setup mounts, so Link throws LinkProviderError there and useNavigate() falls back to a full page load.

  • docs/16-client-navigation.md and docs/20-document-class.md pass a storeIdentifier prop in their examples. The prop does not exist; the store comes from the request.

Related

Source and docs