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

Toaster

componentAdvancedbrowserlive demo

The area where toasts appear, which also turns Salla SDK messages into toasts; TwilightProvider already renders one for you.

import { Toaster, ToastPosition } from '@salla.sa/twilight-theme-engine/components/toast';

In plain words

Toasts need a place on the page to appear. Toaster is that place, and TwilightProvider renders it for every theme, so most themes never write <Toaster />.

It also catches the messages the Salla SDK shows by itself, such as the one after a product is added to the cart, and shows them as toasts in the same style.

What you usually change are the provider's props: toastMobilePosition (where toasts go on phones) and addToCartToast (whether the "added to cart" toast shows).

Signature

function Toaster(props: {
  mobilePosition?: ToastPosition;  // default 'bottom-center', used below 768px wide
  addToCartToast?: boolean;        // default true
}): JSX.Element

type ToastPosition =
  | 'top-left' | 'top-right'
  | 'bottom-left' | 'bottom-right'
  | 'top-center' | 'bottom-center';

// How a theme configures the one TwilightProvider renders:
<TwilightProvider
  toast={true}                       // default: render the Toaster
  toastMobilePosition="bottom-center"
  addToCartToast={true}
/>

Try it live

A message sent through the Salla SDK, shown by the Toaster that TwilightProvider renders on this page.Try this: narrow the browser window below 768px and send it again: the toast moves to the bottom.
Storefront canvas · en · LTR
Runs in the browser…
Controls
What a theme writes
import { getSallaSDK } from '@salla.sa/twilight-theme-engine/utils';

// Nothing to mount: TwilightProvider already renders the Toaster.
export function CopyCouponButton({ code }: { code: string }) {
  const copy = async () => {
    await navigator.clipboard.writeText(code);
    getSallaSDK()?.notify.success('Coupon copied');
  };

  return (
    <button type="button" className="btn btn--outline-primary" onClick={copy}>
      {code}
    </button>
  );
}

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}
          // A bottom navigation bar would cover bottom-center toasts on phones.
          toastMobilePosition="top-center"
          // The cart badge reacts to adds, so skip the default "added to cart" toast.
          addToCartToast={false}
        >
          <Outlet />
        </TwilightProvider>
        <Scripts />
      </body>
    </html>
  );
}

How it behaves

  • TwilightProvider renders it inside <Suspense> once the store is ready, with its toastMobilePosition and addToCartToast props, unless you pass toast={false}. It wraps sonner's Toaster and needs useTwilight(), so it must sit inside the provider.

  • Desktop position is fixed: top-left on right-to-left pages, top-right otherwise (from theme.is_rtl). Below 768px wide it uses mobilePosition. The choice is re-made on window resize only.

  • On mount it calls Salla.notify.setNotifier(…) if window.Salla.notify exists at that moment. From then on every SDK message becomes a toast: a string message is the title, an object uses title or text (and icon as the type); success, error and warning keep their type, anything else is info; all last 3 seconds.

  • It does not wait for the SDK: the effect runs once (it re-runs only when addToCartToast changes). If the SDK has not set up Salla.notify by then, SDK messages keep the SDK's own default notifier, which is the browser's alert().

  • The add-to-cart message (SDK data data.googleTags.event === 'addToCart') is skipped when addToCartToast is false or the theme setting enable_add_product_toast is on, because a theme with that setting renders its own richer toast.

  • Inside the Salla mobile app (Salla.mobile.isEnabled()), SDK messages go to the app's native alert through the ui::alert event instead of a toast.

  • Flash messages: a ?success=… or ?danger=… query parameter present when it mounts is shown once as a success or error toast, and removed from the address with history.replaceState.

  • Styles are Tailwind classes (a white card with border-2 colored by type, rounded-xl, font-primary), top offsets add --app-safe-area-top, and the font is var(--font-main). On unmount it sets the SDK notifier to a function that does nothing.

Gotchas

  • Rendering your own <Toaster /> while the provider's is on shows every toast twice: neither passes a sonner id, and sonner shows id-less toasts in every Toaster. The one mounted last also takes over Salla.notify, and when it unmounts it sets the notifier to a no-op, so SDK messages silently stop until the page reloads (the provider's Toaster does not register again). Render your own only with toast={false} on the provider.

  • toast={false} with no Toaster of your own means toast(…) calls show nothing, and the SDK falls back to its built-in notifier, which is the browser's alert() dialog.

Related

Source and docs