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

Add content to every page with a hook slot

Beginner5 min

Hang an announcement at header:start without copying or editing an engine page.

Goal: show an announcement at the top of every page, without copying or editing any engine page.

Mechanism: a hook slot. The engine leaves named, empty places in its pages and layout (header:start, product:single.form.end, cart:summary.start…). A theme registers a small function, a handler, for one of those names, and whatever the handler returns appears in that place on every page that has it.

1. Try it

The demo hangs content on a slot of the playground's own. Register the same text twice, then change the priority.

A hook slot on this page, and the registry that fills it. Every handler you register renders.Try this: register the same text twice: registration appends, it never replaces. Then raise the priority of a third one.
Storefront canvas · ar · RTL
Runs in the browser…
Controls
Higher runs first. The default is 50.
What a theme writes
import { hookRegistry, HookName } from '@salla.sa/twilight-theme-engine/hooks';

// app/hooks/index.tsx: register once, when the theme starts.
hookRegistry.register(
  HookName.HEADER_START,
  () => <p className="announcement">{'Free delivery over 200 SAR'}</p>,
);

2. Register the handler

Keep every registration in one file. The handler receives a context object: twilight (the store, theme and settings) plus whatever the slot passes, such as product on the product slots. Returning null renders nothing, which is how content follows a merchant setting.

app/hooks/index.tsx
import { hookRegistry, HookName, type HookContext } from '@salla.sa/twilight-theme-engine/hooks';

let registered = false;

export function registerThemeHooks() {
  // register() appends: running this twice would render every handler twice.
  if (registered) return;
  registered = true;

  hookRegistry.register(HookName.HEADER_START, (context: HookContext) => {
    // `twilight` carries the store, theme and settings of the page being drawn.
    const settings = (context.twilight.theme?.settings ?? {}) as Record<string, unknown>;
    // A setting of your own from twilight.json: switched off, the slot stays empty.
    if (settings.show_announcement === false) return null;
    return (
      <p className="bg-primary py-2 text-center text-sm text-white">Free delivery over 200 SAR</p>
    );
  });
}
In engine terms

hookRegistry.register(name, handler, priority = 50) pushes a definition and sorts the list by priority, highest first (src/hooks/HookRegistry.ts). <HookSlot name> calls every handler with { ...context, twilight: useTwilight() } and subscribes to the registry, so a later registration re-renders the slot. HookContext types twilight; your own settings are not in ThemeSettings, hence the cast to Record<string, unknown>.

3. Call it once, when the theme starts

app/router.tsx (excerpt)
import { createRouter } from '@salla.sa/twilight-theme-engine/tanstack';
import { routeTree } from './routeTree.gen';
import { registerThemeHooks } from './hooks';

// Module scope: this file runs on the server and in the browser, before any page renders.
registerThemeHooks();

export function getRouter() {
  return createRouter(routeTree);
}

Traps

Go deeper: hookRegistry, HookSlot, HookName, and the concept How hook slots render.