Indie Polls Embeddable Widget by Releasy CORP

This widget lets you inlcude the Indie Polls feature requests in any website. Indie Polls.

✨ Features

  • 🛡️ Fully Isolated - Shadow DOM encapsulation prevents style conflicts
  • 🚀 Modern Stack - Built with React, TypeScript, and Rollup
  • 🔌 Simple Integration - Single line of code to embed
  • 🛠️ Developer Experience - Hot reload, TypeScript, and modern tooling

How to install the widget in your web app

Basic embed

Add this script tag anywhere in your page (usually before </body>):

<script
src="https://indiepolls.io/widget/widget.js"
defer
data-public-token="YOUR_PUBLIC_TOKEN"
data-class-name="dark"
></script>
  • data-public-token: required. The public token of the poll/widget, generated from the Indie Polls backoffice.
  • data-class-name: optional. Set to dark for dark mode, or leave empty for light.

Optional attributes

The script tag also supports a few optional attributes:

  • data-external-id: a stable identifier for the end user in your system. Used to prevent multiple submissions.
  • data-email: pre-fill contributor email.
  • data-name: pre-fill contributor name.
  • data-embed-target: a CSS selector; if provided, the widget will be rendered inside the matching element instead of attaching to document.body.
  • data-height: desired height for the widget container when using data-embed-target (e.g. 800px, 70vh, 100%).
  • data-layout: set to inline to render the survey in the flow of the page where the snippet sits, without naming a container. Leave it off for the slide-in, modal and banner layouts, which are positioned against the viewport and stay attached to document.body.
  • data-trigger-id: the id of an element on your page that opens the survey, in place of the floating launcher. See below.

Example with more options:

<script
src="https://indiepolls.io/widget/widget.js"
defer
data-public-token="YOUR_PUBLIC_TOKEN"
data-external-id="user-123"
data-email="user@example.com"
data-name="Jane Doe"
data-class-name="dark"
data-embed-target="#indie-polls-widget-container"
data-height="800px"
></script>

Then in your HTML:

<div id="indie-polls-widget-container"></div>

Open the survey from your own element

Give the snippet a data-trigger-id and the floating launcher is not shown: the survey opens, in its slide-in, modal or banner layout, when the visitor clicks the element with that id. Use it when the survey should start from something that looks like part of your site, such as a “Give feedback” link in a menu.

<div id="give-feedback" role="button" tabindex="0">Give feedback</div>

<script
src="https://indiepolls.io/widget/widget.js"
defer
data-public-token="YOUR_PUBLIC_TOKEN"
data-trigger-id="give-feedback"
></script>
  • Any element works: a button, a link or a plain div, and clicks on anything inside it count too.
  • It can be added to the page after the script has loaded, by your framework for example.
  • A div made focusable with tabindex="0" also opens the survey on Enter or Space.
  • The survey’s targeting still applies: on a page or for a visitor it is not meant for, the element opens nothing.
  • An inline survey is always on the page, so it has nothing to open and ignores data-trigger-id.

Who sees the survey (Max)

On the Max plan, the survey’s Widget tab can limit who the widget shows to. Below Max these settings are kept but not applied, and the widget shows to everyone.

  • Pages: an exact website domain, and path patterns one per line.
  • Audience: rules on what your page knows about the visitor, passed as JSON in data-attributes, such as plan is pro, signup_days greater than 30, role is not admin or company contains acme. Every rule has to hold. Text is compared ignoring case; greater than and less than compare numbers. The same attributes are stored with each answer, so results can be broken down by them.
  • Devices: desktop, mobile, tablet, or any mix. None ticked means all.
  • Event: the survey waits until your page tells the widget something happened, optionally a number of times. Counts are kept per visitor in their browser’s local storage.
<script
src="https://indiepolls.io/widget/widget.js"
defer
data-public-token="YOUR_PUBLIC_TOKEN"
data-attributes='{"plan":"pro","signup_days":42,"role":"member"}'
></script>

<script>
// Once the widget script has loaded, from anywhere in your page:
window.IndiePolls.track('checkout_completed');
</script>

IndiePolls.track(name) takes the event’s name, as typed in the Widget tab. This is about showing the survey to the people it is meant for, not about access: the rules run in the visitor’s browser, on what your page says.

Styling with custom CSS (Max)

On the Max plan, the survey’s Customizations tab takes a stylesheet of your own. It applies to the hosted survey page and to the widget, where it stays inside the widget’s shadow root and never reaches the rest of your page. @import and a closing </style> tag are refused. The editor’s Insert example button fills in the starter stylesheet below.

Style the ip- classes and the variables listed here. They are the same on the hosted page and in the widget, and they stay the same between releases; the other classes in the markup are utilities that can change at any time.

  • Set the variables on .ip-survey, on both surfaces. In the widget that is the element inside the shadow root; :host does not work, since the defaults are set on .ip-survey.
  • Start other selectors with .ip-survey, as in .ip-survey .ip-option, so they win over the survey’s own styles in light and dark mode.
  • A chosen answer has .ip-option-selected and aria-pressed="true".
  • In dark mode, the survey uses --ip-primary-dark and --ip-primary-contrast-dark when you set them, and the light ones when you don’t. Inside .ip-survey, var(--ip-primary) and var(--ip-primary-contrast) then give the colours for the current mode, so your own rules that read them follow along.

Variables

  • --ip-primary: Primary buttons, chosen answers and focus rings. Default: blue on the hosted page, amber in the widget.
  • --ip-primary-contrast: Text on the primary colour. Default: white.
  • --ip-primary-dark: The primary colour in dark mode. Default: the same as –ip-primary.
  • --ip-primary-contrast-dark: Text on the primary colour in dark mode. Default: the same as –ip-primary-contrast.
  • --ip-radius: Corners of answers, inputs and buttons. Default: 0.25rem.
  • --ip-font: The survey’s font. Default: the default sans-serif.

Classes

  • .ip-survey: The whole survey. Set the variables here.
  • .ip-panel: The box the survey sits in: the card on the hosted page, the panel in the widget.
  • .ip-header: The top of the survey, holding the title.
  • .ip-logo: The logo in the widget’s header. Widget only.
  • .ip-title: The survey’s title.
  • .ip-intro: The intro screen, before the first question.
  • .ip-progress: “Question 1 of 3”, above each question.
  • .ip-question: The current question.
  • .ip-question-prompt: The question’s text.
  • .ip-option: An answer choice, including each point of a scale or rating.
  • .ip-option-selected: A chosen answer, which also has aria-pressed set to true.
  • .ip-scale: The row of points of a scale (NPS, CSAT, stars, Likert) or rating.
  • .ip-input: A text, number, date or email box, and the name and email fields.
  • .ip-button-primary: Start, Next and Submit.
  • .ip-button-secondary: Back.
  • .ip-outro: The thank-you screen after submitting.
  • .ip-launcher: The floating button that opens the widget. Widget only.

Example

/* Starter stylesheet: keep what you need, delete the rest.
   It works on the hosted survey page and in the embedded widget. */

/* Theme: the brand colour, corners and font, changed in one place. */
.ip-survey {
  --ip-primary: #0f766e;          /* buttons, chosen answers, focus */
  --ip-primary-contrast: #ffffff; /* text on the primary colour */
  --ip-primary-dark: #2dd4bf;     /* the same two in dark mode */
  --ip-primary-contrast-dark: #042f2e;
  --ip-radius: 10px;              /* answers, inputs and buttons */
  --ip-font: Georgia, "Times New Roman", serif;
}

/* Parts: start each selector with .ip-survey so it wins over ours.
   Inside the survey, var(--ip-primary) is the dark colour in dark mode. */
.ip-survey .ip-question-prompt {
  font-size: 1.125rem;
}

.ip-survey .ip-option-selected {
  font-weight: 600;
}

.ip-survey .ip-button-primary {
  background-color: var(--ip-primary);
  color: var(--ip-primary-contrast);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

Dynamic example (like test/index.html)

You can also inject the widget dynamically and assign a random external id at runtime:

<script>
const dynamicUser = Math.random().toString(36).slice(2);
const s = document.createElement('script');
s.async = true;
s.src = 'https://indiepolls.io/widget/widget.js';
s.dataset.publicToken = 'YOUR_PUBLIC_TOKEN';
s.dataset.externalId = `user-${dynamicUser}`;
s.dataset.className = 'dark';
document.body.appendChild(s);
// Optionally, set s.dataset.embedTarget, s.dataset.height, etc.
</script>

Changelog

1.4.0

  • Dark mode colours for custom CSS: set --ip-primary-dark and --ip-primary-contrast-dark on .ip-survey and a dark widget uses them for its buttons, chosen answers, tints and focus rings, as the hosted survey page does. Each falls back to its light one, so a dark widget looks as before until they are set.

1.3.0

  • Question count: each question shows where it sits in the survey, e.g. “Question 2 of 5”, as on the hosted survey page, with the same .ip-progress class for custom CSS. When a branch skips ahead, the count skips with it.

1.2.0

  • Styling with custom CSS: stable ip- classes and --ip-* theme variables for a Max survey’s own stylesheet, the same as on the hosted survey page. The stylesheet now also wins a tie with the widget’s own rules.

1.1.0

  • data-trigger-id: open the survey from an element on your page instead of the floating launcher.

1.0.0

  • Initial public release

Common questions

How do I embed a poll on my website?
Create the poll, copy the one-line script tag, and paste it where you want the poll to appear. There is no iframe and no build step, so it works the same on a static site, a landing page or inside an app.
Does the widget work with React, Vue or Next.js?
Yes. The widget is a plain script tag that mounts into whatever element you point it at, so it does not care which framework rendered the page around it. In a component, add the script once on mount and give it a target element.
Will it need a cookie banner?
No. The widget collects what it needs to count a vote and nothing more, so adding a poll does not turn into a privacy review.
Can I style the widget to match my site?
Yes. The widget inherits a light or dark class from you and exposes its own class names, so it can be themed without forking anything.
How much does it slow the page down?
The script is small and loads asynchronously, so it does not block rendering. It is designed to be left permanently on a pricing or docs page rather than added and later removed.