Docs
grovs.io Start for free

In-app messages

Show in-app messages from the Grovs dashboard on your website, hold them back on some pages, and react to clicks and answers.

Requires Web SDK 2.1.0

In-app messages are dashboard campaigns: a banner, modal, full-page cover, question or your own HTML. The SDK downloads live campaigns, picks one in the browser for each app open, screen view or event, and draws it over your page. To create campaigns, see In-app messages.

No code is required. Once the SDK is configured, campaigns show. Use Grovs.inApp to keep messages off some pages, pause them during a flow, or react to what the visitor did.

Requires Web SDK 2.1 or later. Messages are on by default, but nothing shows until a campaign targets web visitors. They replace the messages inbox removed in 2.1.

How messages reach the page

  • Sync. After configure, when the visitor returns to the tab (visible or focused) if the last sync is over a minute old, and every 15 minutes while the page is open.
  • Triggers. App open (a new session), a screen view (automatic route changes or trackScreenView), or a custom event from track, optionally filtered by its properties.
  • Decision. Schedule, conditions and frequency caps are checked in the browser. If several campaigns match one event, the highest priority shows.
  • One at a time. While a message is on screen, the newest highest priority match waits. It shows once the screen is free, if it still qualifies.

Messages render in a shadow root, so CSS doesn't leak either way. They don't show during server rendering, while consent is pending under requireConsent, or while the SDK is disabled.

App version conditions compare against the appVersion passed to configure. Without one, those conditions fail.

React to messages

javascript
import Grovs from "grovs";
 
Grovs.inApp.setCallbacks({
  // Return false to stop a message. It gains no view, and nothing else shows for that event.
  beforeShow: (campaignId) => !checkout.isOpen,
  onShown: (campaignId) => {},
  // buttonId is null for a click on the message itself. payload is set for a key-value action.
  onClicked: (campaignId, buttonId, payload) => {
    if (payload?.page) router.push(payload.page);
  },
  // reason: close_button, backdrop, app or orientation
  onDismissed: (campaignId, reason) => {},
  // Called once an answered question has closed. value is a number, or the option's id for a choice.
  onAnswered: (campaignId, value, comment) => {},
});

Each setCallbacks call replaces the previous callbacks. It and every other Grovs.inApp method work before configure, and settings carry over when configure runs again.

CallbackWhen it's called
beforeShow(campaignId)Before a message shows. Return false to stop it.
onShown(campaignId)The message is on screen and counted as viewed.
onClicked(campaignId, buttonId, payload)A button or the message was clicked. payload carries the pairs of a key-value action.
onDismissed(campaignId, reason)The message closed without a click. orientation means the window changed to a disallowed shape; not a dashboard dismissal.
onAnswered(campaignId, value, comment)A question was answered and the message closed. comment is null unless one was sent.

Button actions

ActionWhat the SDK does
CloseCloses the message.
Open URLReports the click, then navigates the current tab to the URL.
Key-valueCloses the message and passes the pairs to onClicked as payload.
Request push permission, request app rating, open storeNot available on the web. Campaigns that use them aren't shown in browsers.

Hold messages back

javascript
Grovs.inApp.pause();                       // hold messages; the newest highest priority one waits
Grovs.inApp.resume();                      // show the one that waited, if it still qualifies
Grovs.inApp.discard(true);                 // drop the waiting one and stay paused; true also closes the one on screen
Grovs.inApp.setExcludedScreens(["Checkout", "Sign in"]);  // no message on these screens

Excluded screens match tracked screen names (from trackScreenView or automatic route tracking). Messages wait until the visitor leaves.

Questions

A message can ask for a star rating (1 to 5) or one choice from a list. One click answers. A comment box (up to 500 characters) and a thank-you text can follow. An answered campaign isn't shown again in that browser. The answer is an event (in_app_answered) that can trigger another campaign.

Custom HTML messages

The HTML template shows your page in a sandboxed srcdoc iframe (sandbox="allow-scripts", no same-origin access). It can't read your page, cookies or storage. The SDK injects window.grovs before your scripts run:

MemberDescription
grovs.close()Closes the message.
grovs.track(name, properties)Tracks a custom event with campaign_id added; the message stays open. The first call counts as the message's click. Names are 1 to 100 characters and can't be reserved names.
grovs.open(url)Opens a link and closes the message. Only works within 2 seconds of a click or key press inside the message.
grovs.setAttribute(key, value)Sets a user attribute. value is a string, number, boolean or null.
grovs.requestPushPermission()Resolves "unsupported" in browsers.
grovs.user, grovs.eventRead-only copies of the user's attributes and the triggering event's properties.
grovs.capabilities.pushPermissionAlways "unsupported" in browsers.

Without script, use data-grovs-action="close", data-grovs-action="open" with data-grovs-url, or data-grovs-action="track" with data-grovs-event. Plain <a href> links go through grovs.open; #fragment links scroll inside the message.

Personalization. {{user.first_name}} and {{event.plan}} are filled in text and in alt, title, value, placeholder, aria-* and data-* attributes. Add a fallback with {{user.first_name | default: "there"}}. If a field has no value and no fallback, the message isn't shown.

Links. javascript:, vbscript:, data:, blob:, file:, about:, intent: and plain http: links are refused.

Size. Full-page cover, modal or banner. A banner takes up to 30% of screen height, or 45% on short screens (500px or less, like a sideways phone). Escape closes a dismissible modal or full-page message, even with focus inside the frame. It doesn't close banners.

Content-Security-Policy

The iframe inherits your page's Content-Security-Policy; the SDK can't override it. A CSP must allow:

  • script-src: inline scripts via 'unsafe-inline', with no nonce, hash or 'strict-dynamic' in script-src (or default-src if there's no script-src). Browsers ignore 'unsafe-inline' when any of those is present, so such policies can't run HTML messages at all.
  • img-src, media-src, font-src: the hosts for the message's pictures, video and fonts, for example your CDN or https:. img-src also covers built-in template pictures.
  • style-src 'unsafe-inline': if the message uses <style> or style="".
http
Content-Security-Policy: script-src 'self' 'unsafe-inline'; img-src 'self' https:; style-src 'self' 'unsafe-inline'

A blocked message isn't ready within 5 seconds. The SDK skips it and reports GrovsError.inAppDisplayFailed (5) to onError once, naming the CSP.

Built-in templates need no script or style exception and work under any policy, including nonce-, hash- and 'strict-dynamic'-based ones.

Test messages

Send test on a dashboard campaign sends it to every device of one person, matched by Grovs.setUserIdentifier. The browser shows it at its next sync, ignoring triggers, conditions and limits, once the screen is free and messages aren't paused. Switch tabs and come back after a minute to trigger that sync.

A test message isn't counted or reported. A test using an action browsers can't perform is skipped.

Orientation

A campaign targets portrait, landscape or both. A window wider than tall is landscape. The message shows only in that shape and closes if it changes, reported to onDismissed with reason orientation.

Debugging

javascript
Grovs.setDebugLevel("info");
Grovs.inApp.setDebug(true);

setDebug(true) logs, at info level, each campaign considered for an event and why it was skipped.

Reserved event names

The SDK reports message views, clicks, dismissals and answers as events. track() and grovs.track refuse these names:

in_app_viewed, in_app_clicked, in_app_dismissed, in_app_answered, in_app_commented, in_app_held, push_open, push_delivered, push_held, journey_entered, journey_held, journey_path, plus the system names such as app_open, screen_view, install and view.

See also

Last updated 2026-10-26
Was this page helpful?