Skip to main content
The Claimr widget works in any JavaScript framework. There is no npm package to install: you load one script, claimr.min.js, and it renders the widget inside an element you choose. This page explains how that script behaves inside a single-page app (SPA), and lists the rules that keep an integration stable across route changes, re-renders, and server-side rendering. Read this page first, then follow the guide for your framework:

React

Vite, Create React App, React Router.

Next.js

App Router and Pages Router.

Vue and Nuxt

Vue 3 with Vite, and Nuxt 3 or 4.
Building with an AI coding agent such as Claude Code, Codex, or Cursor? See Use Claimr docs with AI coding agents for a ready-made rules file you can drop into your project.

Before you start

Collect these values from the Claimr admin panel. Open your campaign, click Publish, then click Get embed code.

How the loader works

When claimr.min.js runs, it does the following:
  1. Finds the script element with id="claimr-script" and reads every data-* attribute from it. The ID is fixed: the loader looks it up by that exact name.
  2. Creates the SDK object at window.claimr and posts a claimr::ready message to the page window.
  3. Looks for the element whose ID matches data-container. If it exists, the loader appends the widget <iframe> to it. It also appends a full-screen popup layer (#claimr-contest-popup, position: fixed, z-index: 1000) to <body>.
  4. Watches <body> with a MutationObserver:
    • When the container element appears, the loader attaches the widget to it.
    • When the container element disappears, the loader removes the widget iframe and the popup layer.
  5. When the widget iframes finish loading, the loader sets window.claimr.is_claimr_ready to true and posts a widget::ready message to the page window. Any login() calls made earlier run at this point.
  6. If data-autoresize is set, the loader writes the container’s height as an inline style every time the widget content changes size.
The key consequence for SPAs: the script is loaded once, and the widget follows the container. You show and hide the widget by rendering or removing the container element, exactly like any other component. You never re-inject the script to show the widget on a new route.

Integration rules

Every framework guide in this section follows these rules. If you write your own integration, follow them too.
1

Load the script once, from the app root

Inject the script from your root component, root layout, or app plugin. Do not inject it from the component that renders the widget, and do not inject it on every route. Guard the injection with document.getElementById('claimr-script') so that React Strict Mode, hot module reload, and double mounts stay idempotent.
2

Give the script the ID claimr-script

The loader reads its configuration from document.getElementById('claimr-script'). Any other ID breaks the integration. Only one Claimr script can exist on a page.
3

Render the container as an empty element with a stable ID

Render a single empty <div> whose id matches data-container. Do not render children inside it and do not bind an inline height style to it: the loader owns its content and, with autoresize, its height. Style it with a class instead.
4

Let the loader attach and detach the widget

To hide the widget, remove the container (conditional rendering, route change). To show it again, render the container again. Do not call destroy() when a component unmounts: destroy() also disconnects the observer and clears the stored wallet session, so the widget will not come back on the next mount.
5

Read window.claimr at call time

Always call window.claimr?.method() when you need it. Do not store the object in a variable, module constant, or state: reloading the script replaces it with a new object.
6

Assign callbacks once, right after the script loads

Callbacks such as on_user_info are plain properties, so each one has a single owner. Assign them in the same place that loads the script, inside the script’s load handler, and fan the data out through your app’s state (React context, a Vue reactive object, Pinia, Nuxt useState). Do not assign them from individual components.
7

Wait for widget::ready before calling widget methods

Most methods send a message into the widget iframe. If the iframe has not loaded yet, the message is lost. See Method readiness.
8

Run Claimr code in the browser only

window and document do not exist during server-side rendering. Load the script from an effect, a mounted hook, or a client-only plugin. Rendering the empty container on the server is fine.
9

Keep the API token on the server

Generate user tokens in a server route or server component, then pass only the resulting user token to the browser.

Method readiness

The loader keeps a few values and replays them when the widget loads. Everything else is sent straight to the iframe and is dropped if the widget is not ready. To wait for readiness, check window.claimr?.is_claimr_ready, and if it is false, listen for the next widget::ready message. The widget::ready and claimr::ready messages are posted by the loader to your own window, so filter on event.source === window:
widget::ready only fires while a container is mounted. On a page without a container, the promise above stays pending until the user navigates to a page that renders one.

The shared loader module

Every framework guide uses the same framework-agnostic module, claimr.ts. It loads the script once, assigns callbacks on every fresh instance, exposes readiness helpers, and declares TypeScript types for window.claimr. Copy it into your project as src/lib/claimr.ts (or lib/claimr.ts in Nuxt).
lib/claimr.ts

Configuration and environment variables

The organization and campaign IDs are public values, so it is fine to expose them through your bundler’s public environment variables. The API token is secret and must only be read on the server. Script attributes are read once, when the script runs. Changing them later has no effect. To change settings at runtime, use the SDK method instead: See Widget attributes for every data-* attribute and the SDK guide for every method and callback.

Identify your users

Pick one of these sign-in models. They are configured per campaign in Settings > Sign-in options. The user token endpoint returns { "success": true, "data": { "token": "..." } }. The token does not expire, so you can store it with the user record instead of requesting it on every page load.

Common mistakes