Slots overview
Every named slot the engine renders, where it sits on each page, and how to fill it from theme code or an installed app.
Read this page when you want to add content to a page without forking the component that renders it, or when you need to know which slots exist.
A slot is a named spot in the markup, written <HookSlot name="header:start" />. Anything registered under that name appears there: your theme code, the engine's own defaults, and apps installed on the store. If nothing is registered, the slot renders nothing (or its fallback).
Use a slot in three steps
Pick the slot. Find the page in the catalog below and choose a name, for example product:details.end. Each page lists where the slot sits and what context its handler receives.
Register a handler once, at app start. Use hookRegistry.register (or defineHooks for several at once) in a module your app imports at start. The starter theme keeps them in app/hooks/index.tsx and calls registerThemeHooks() from app/router.tsx, as in Add content to a slot.
import { hookRegistry, type HookContext } from '@salla.sa/twilight-theme-engine/hooks';
hookRegistry.register('header:start', ({ twilight }: HookContext) => (
<div className="announcement">Free shipping on orders over 200 {twilight.currency?.code}</div>
));Inside a component you can use useHook instead. Read how its cleanup works first.
Render. Nothing else to do: the engine's pages already render the slot. If you replaced a layout or a page, you must render the slot yourself with HookSlot.
Slot catalog
Names use area:part or area:part.position. Most areas come in .start and .end pairs. Each page below says where the slot sits, what context it passes, and shows an example.
| Area | What it covers | Page |
|---|---|---|
| Page frame | body:start, body:end, head:start, head, head:end, header:start, header:end, footer:start, footer:end, copyright | Page frame |
| Home and catalog | Home page, product list, brands, search, not found page | Home and catalog |
| Product page | Product details, description, form, related products | Product page |
| Cart and checkout | Cart, cart summary, submit button, thank-you page | Cart and checkout |
| Account | Orders, order details, profile, settings, wallet, wishlist, notifications | Account |
| Blog | Blog index, article, category, tag, author | Blog |
No slots inside drawers and modals
The engine renders no slot inside a drawer, modal or popup (for example the mini cart or the language switcher). A name the engine never renders does nothing, so check the catalog for the exact name before you register.
Resolving conflicts and edge cases
Order: priority, then registration
register(name, handler, priority = 50). Handlers for one name are sorted by priority, higher first. Handlers with the same priority keep the order they were registered in. Every handler renders, one after another, so several handlers on one slot stack up.
hookRegistry.register('footer:end', () => <Second />, 10);
hookRegistry.register('footer:end', () => <First />, 90); // renders firstThe engine registers its own defaults with priorities 80 to 100 on body:start (tracking, store-closed notice), body:end, cart:items.start and cart:submit.start. Use a priority below 80 to render after them.
fallback is shown only when the slot is empty
A slot can have a fallback, which a component passes to HookSlot for content to show when nothing is registered. The moment you register any handler for that name, the fallback is replaced, not added to. To keep the default content, render it yourself in your handler.
useHook removes only its own handler
useHook registers on mount and, on unmount, removes the handler it registered. Other handlers for the same name stay, including the engine's defaults.
hookRegistry.register works the same way: it returns a function that removes the one handler it added.
const remove = hookRegistry.register('header:start', () => <Promo />);
remove(); // removes only this handlerhookRegistry.clear(name) is different: it removes every handler for the name. Avoid calling it on a slot the engine fills.
The safe pattern is to register once at startup and never clear:
import { defineHooks } from '@salla.sa/twilight-theme-engine/hooks';
defineHooks({
'header:start': () => <Announcement />,
'footer:end': [
{ handler: () => <Newsletter />, priority: 60 },
{ handler: () => <Badges />, priority: 40 },
],
});Content from useHook shows only while its component is mounted, and only in the browser: it is never in the server HTML. Use it for content that belongs to one component, and register at startup for content every page should have.
A handler that throws renders nothing
If a handler throws, that handler renders nothing and the other handlers still render. The error is logged only in development. Return null (or nothing) to render nothing on purpose.
Context
A handler receives the context the slot passes, plus twilight: the full Twilight context (store, theme, currency and more). If your handler needs data the slot does not pass, read it with the matching hook inside the component you return, such as useStore. Type the argument with HookContext:
import type { HookContext } from '@salla.sa/twilight-theme-engine/hooks';
hookRegistry.register('header:start', ({ twilight }: HookContext) => (
<span>{twilight.store.name}</span>
));Apps installed on the store
Besides your handlers, every slot renders a <salla-hook name="..."> element where apps installed on the store inject their content.
- It renders on the client only, to avoid a hydration mismatch when an app changes the DOM before React hydrates.
- The engine's
head:*slots passssr, so their element is also in the server HTML. Set thessrprop on your own slot only if no app script will change it. - Add this rule to your global CSS so the element takes no space:
salla-hook { all: unset; display: none; }Your own slot name for apps
You can render <HookSlot name="sidebar:top" /> anywhere in your theme. To let installed apps mount into a name of yours, declare it with registerThemeHookSlots. The Salla SDK only lets apps mount into names on its allow list, and this call adds your names to it.
import { registerThemeHookSlots } from '@salla.sa/twilight-theme-engine/utils';
registerThemeHookSlots('sidebar:top', 'sidebar:bottom');Call it at module scope, so it runs before the page loads. Your own handlers in the registry do not need it.
You replaced a layout or a page
The slots live inside the engine's Header, Footer, pages and TwilightProvider. If you swap one for your own component, its slots go with it. Render <HookSlot /> with the same names in your version so handlers and apps keep working. The page-frame slots body:start and body:end are rendered by the provider, so they survive a layout swap.
Next
- Page frame slots is the best page to start with.
HookSlotanduseHookdocument the API.- Add content to a slot walks through one end to end.