Docs
grovs.io Start for free

Custom HTML messages

Show your own HTML page as an in-app message, with video, audio and a small JavaScript bridge back to the SDK.

All Cloud plans and self-hosted Enterprise · Requires iOS SDK 3.1.0, Android SDK 3.1.0, Web SDK 2.1.0

The Custom HTML template shows a page you write, in a sandboxed web view on iOS and a sandboxed iframe on the web. Use it when built-in templates aren't enough: a video, a countdown, a three-slide tour or your own brand layout.

Triggers, audience, limits, priority, schedule, experiments, journeys and test messages work as for any other template.

Custom HTML needs an app built with iOS SDK 3.1.0 or Android SDK 3.1.0 or later, or a site on Web SDK 2.1.0 or later. Older versions skip the message silently.

Layouts

The page's height sets the message's height, up to the layout's maximum.

LayoutHow it shows
Full screenFills the screen inside the safe areas. The reported height is ignored.
Card in the middleA card over a dimmed backdrop, at most 420 points wide and 90% of the screen tall. Taller pages scroll.
Banner at the topFull width, at most 30% of the screen tall. The app stays usable underneath.
Banner at the bottomAs above, at the bottom.

On an iPhone in landscape (under 500 points tall), a banner can grow to 45% of the screen.

The SDK always draws its own close button in the top-right corner, whatever the close-button setting, so a broken script can never trap anyone. Don't draw your own X. Making the message non-closable only disables the backdrop tap, swipe and Escape key; the SDK's close button stays.

Build the page

1
Pick Custom HTML

In the composer's Message step, set Template to Custom HTML. Pick a Layout, and an Orientation if the message only works one way.

2
Start from an example

Start from an example offers eight working pages: Promo video, Product launch, Push permission primer, Countdown offer, Feature tour, Feedback, Announcement and Win-back offer. Or choose Start blank. Every example works on phone and desktop, in light and dark mode.

Picking an example later replaces your HTML, and the code editor can't undo it.

3
Edit the text, pictures and buttons

Quick edit turns every element marked data-grovs-edit="Label" into a form field: text, image, video or audio file, button label and action, or colour. Script-controlled elements are offered as text only.

For anything else, switch to Code. The editor autocompletes grovs.*, the data-grovs-* attributes and {{ user.x }} fields. Snippets inserts a "No thanks" button, a tracked button, a link button, a push permission button, a sized video, editable text and personalized text. How it works opens a short reference. The editor shows the size against the 64 KB limit and runs the server's checks as you type.

4
Upload media

Upload pictures, video and audio in the Media panel and copy each file's link into your HTML. Each file shows Used in this message or Not used yet. Files still referenced in the HTML can't be deleted.

5
Preview it

Preview runs your page with the real bridge on Phone, Phone landscape or Browser, in light or dark. Sample values fill personalization fields. Each bridge call appears under What the message did, so you can check what buttons close, track and open.

Limits

WhatLimit
HTML size64 KB of UTF-8. Doesn't count toward the 16 KB limit for the rest of the message.
ImagesPNG, JPEG or GIF, up to 1 MB each
VideoMP4, up to 20 MB each
AudioMP3 or M4A, up to 10 MB each
Files per message30 files and 100 MB in total

Media downloads when the message shows, not at sync. Export videos with "fast start" (web-optimized) so playback starts before the whole file arrives.

Duplicating a message copies only the files its HTML still uses. A message's files are deleted with it.

The server checks your HTML on save, and the SDK re-checks links on tap.

Links (<a href> and data-grovs-url) may be:

  • https:// with a host name
  • mailto:, tel: or sms:
  • a #fragment, which scrolls within the page
  • your app's own scheme, such as medly:// or instagram://

Refused: http:, javascript:, vbscript:, data:, blob:, file:, about:, intent:, and relative links (the page has no base address).

Resources (src, poster, srcset, <link href> and url() in a style attribute) must be https:. Images may also be data:image/….

<base> and <meta http-equiv="refresh"> aren't allowed. Give every <img> and <video> a width and height, or an aspect-ratio, or the message jumps as it loads. The builder warns when one is missing.

Personalize

Write fields in text as in any message:

HTML
<h1>Hi {{ user.first_name | default: "there" }}</h1>

Fields are filled in text and in the alt, title, value, placeholder, aria-* and data-* attributes. They're never filled in scripts, styles, <template>, <textarea> or any URL attribute. A {{ inside href or src is refused on save. To build a personalized link, read the value in a script:

HTML
<script>
  const link = "https://medly.app/plan?name=" + encodeURIComponent(grovs.user.first_name || "");
</script>

If a field has no value and no fallback, the message is skipped for that trigger and not counted, as with built-in templates.

The JavaScript bridge

The SDK injects a grovs object before any of your scripts run.

CallWhat it doesCounts as the click
grovs.close()Closes the messageNo
grovs.track(name, properties?)Records a custom event with the message's campaign_id added. Names are 1 to 100 characters and can't be a reserved name such as in_app_viewed or installYes, if it's the first
grovs.open(url)Opens a link allowed by the rules above, then closes the message. Ignored unless it follows a real tap in the pageYes, if it's the first
grovs.setAttribute(key, value)Sets an attribute of the person, as the SDK's own attribute call doesNo
grovs.requestPushPermission()Asks for notification permission. Returns a promise of "granted", "denied" or "unsupported". If the person already decided, it returns that answer without asking againNo
grovs.userThe person's attributes, read-only
grovs.eventThe properties of the event that triggered the message, read-only
grovs.capabilities.pushPermission"prompt" while the app can still ask, otherwise "granted", "denied" or "unsupported"

Calls made before the message is on screen aren't queued: requestPushPermission() rejects with not_shown, and the others are dropped.

On the web, push permission is always "unsupported". A push primer should hide its button unless grovs.capabilities.pushPermission is "prompt". The gallery's primer does this.

Buttons without code

Most messages need no JavaScript. Put an action on any element:

HTML
<button type="button" data-grovs-action="close">No thanks</button>
<button type="button" data-grovs-action="open" data-grovs-url="https://medly.app/plans">See plans</button>
<button type="button" data-grovs-action="track" data-grovs-event="tour_finished">Got it</button>

A plain <a href> also works: a tap opens the link and closes the message. A link to #id scrolls to that element instead. If your own click handler calls preventDefault(), the bridge leaves the link alone.

iOSWeb
https:// linksOpened by the systemOpened in the same tab
Other schemes (medly://, mailto:)Opened by the system. Nothing happens if no app handles themHanded to the browser

How the message shows

The page loads hidden at its final width. The SDK shows it, and counts the view, once the page has finished parsing and reported a height above zero. A Full screen page only needs to finish parsing. Images still loading may then grow the message up to its maximum.

If the page isn't ready within 5 seconds, the SDK drops it without showing or counting it.

On iOS the page runs in a WKWebView that keeps no cookies or storage after closing. Inline video plays without a tap. Navigation away from the page is cancelled; links go through the bridge. Embedded frames, such as a YouTube player, can navigate.

On the web the page runs in an <iframe sandbox="allow-scripts" srcdoc> with no access to your site's origin. If the page navigates itself, the message closes.

Content-Security-Policy on your site

On the web, the message's iframe inherits your page's Content-Security-Policy, and the SDK can't change it. To show HTML messages, the policy must allow:

  • Inline scripts. The bridge and your page's scripts are inline, so script-src (or default-src without script-src) needs 'unsafe-inline' and no nonce, hash or 'strict-dynamic'. Browsers ignore 'unsafe-inline' when any of those is present, so such policies can't run HTML messages at all.
  • Your media hosts in img-src, media-src and font-src. img-src also covers built-in template pictures.
  • style-src 'unsafe-inline' if the page uses <style> or style="".

A blocked message never starts. The SDK skips it and reports inAppDisplayFailed (code 5) to your error callback once, naming the CSP. Built-in templates work under any policy.

Test messages

In a test message, grovs.track and grovs.setAttribute are logged, not sent. Everything else behaves as in production.

Self-hosting: the preview page

The builder's preview is a small page served by your API at /in_app/preview, embeddable only by the dashboard's own origins. If your dashboard runs on a non-default origin, list every allowed origin in IN_APP_PREVIEW_ORIGINS, comma-separated. The list replaces the defaults.

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