<InAppMessages/> shows the messages the current user is eligible for.
Eligibility is decided server-side: Galinum evaluates every running campaign’s
targeting against the user’s traits and event history, assigns an A/B variant
(stable per user, weighted), and creates a delivery record.
One message per page view
The SDK prefetches eligible messages when the user is identified and keeps them in memory. Each page view then picks from that cache synchronously, so no request sits on the render path.- A user sees at most one message per page view, across every mounted
<InAppMessages/>in your app. - Messages appear at page load or right after a navigation. Nothing ever pops into a screen the user is already reading.
- Dismissing a message does not release the page view. Navigation is the only unlock.
- A message still on screen when the user navigates simply leaves. It sends no dismissal, so the same message can appear again on a later page view.
- Fetches happen at identify, on navigation (feeding later page views), and after the user resolves a message. There is no polling timer.
- Each fetch first waits for the
track()requests already in flight (up to a short fail-open timeout, 2 s), so event-based targeting normally sees the events your app just fired. If a track request stalls past the timeout, the fetch proceeds anyway: delivery is never blocked by tracking. - Actually rendering counts as the impression — when a message mounts, the
SDK reports
shownfor the delivery. Fetching alone never marks a message shown, so a cached message the user never sees is not counted as an impression. - A user gets each campaign at most once: after they dismiss, click, or convert, the message doesn’t come back.
Targeting screens
A campaign can name the screens it belongs on. Itspages patterns are matched
in the browser against the current pathname:
- A pattern starts with
/.*matches any characters, including/. - Everything else is literal and case-sensitive. Trailing slashes are normalized, and query strings and hashes are ignored.
- A campaign with no patterns is eligible everywhere.
pages answers where a message may show; the audience answers who sees
it. Your agent sets both through the campaigns API.
Images are warmed in the background and every message reserves its image area,
so a slow image never delays the text or shifts the layout.
The default renderer
With no props, each message states its own presentation incontent.presentation:
"toast"— a compact card fixed to the bottom-right corner, showing the variant’stitle,body, and an optional CTA button. When the message also carriescontent.media, the image renders as a small thumbnail (~48 px) inside the card."modal"— a large centered announcement over a full-screen backdrop. With media, the image sits at the top as the visual focus, with the title, body, and a prominent CTA below; without media the modal renders text only. On desktop the modal is a constrained card (~480 px wide); on mobile it is near-full-width and viewport-safe. Animated GIFs animate.
presentation; a cached or older payload without it falls back to the
original rule (media renders as a modal, no media as a toast).
Dismissals and CTA clicks are reported back automatically in both layouts.
CTA URLs are validated — only http:, https:, and mailto: schemes
render, so a compromised campaign can’t inject javascript: URIs into your
page. Media URLs are stricter still: only http:/https: images render.
The modal follows accessible dialog conventions: it takes focus when it
opens, keeps keyboard focus inside, closes on the close button, on
Escape, or on a backdrop click, and returns focus to where the
user was. The image’s alt text comes from the campaign (media.alt, or
empty for images marked decorative). If the image fails to load, the text
content and dismissal controls still render. Only one message is on screen at
a time, so a toast never sits dimmed behind a modal backdrop; when both a
modal and a toast are eligible for the same page view, the modal goes first.
All presentation is inline styles — the SDK injects no stylesheets into your
page (inherited properties like font-family still apply, so the cards pick
up your app’s typeface).
Theming
The built-in renderer has a light and a dark palette. Thetheme prop selects
it: "light", "dark", or "auto" (the default).
"auto" reads the computed color-scheme of your <html> element, the
standard way a page declares its scheme, and looks at which keywords it
contains:
darkwithoutlight(dark,only dark) renders the dark palette.- Both keywords (
light dark,dark light) follow the user’s OS preference. - No
darkkeyword renders the light palette.
color-scheme, pass the value yourself:
theme styles only the built-in toast and modal. A custom render is
unaffected.
Custom rendering
Passrender to fully control the UI while Galinum keeps handling targeting,
variant assignment, and measurement:
message.content is the variant’s JSON payload — the default shape is
{ title?, body?, cta?, media?, presentation }, but campaigns can carry any
JSON your renderer understands. The message arrives untouched
(content.media is { url, alt?, decorative? } and
content.presentation is "toast" or "modal"); the layouts above apply
only to the built-in renderer, so use or ignore presentation as you like.
Impressions still count on render: when your render function returns
content for a message, the SDK reports shown for that delivery
automatically. Return null to render nothing — no impression is recorded,
and the next matching message takes its place on this page view.
Call the actions callbacks so Galinum can measure: