Overview
The Survey widget renders multi-question surveys inside your product — open text, scale, NPS, single/multi choice, boolean. It fires when a URL pattern matches, a custom JS event dispatches, or a time-on-page threshold elapses. Surveys can also be opened by id from your own UI code via SeggwatSurvey.show(survey_id, { force: true }).
The widget pulls live survey definitions from the server on load, so customers paste the snippet once and you author surveys from the dashboard.
Try every display mode at the live demo. The demo bypasses the cooldown so you can re-fire the survey indefinitely.
Any plan can build and preview surveys as drafts. Publishing a survey (setting it Live so the widget serves it) requires the Pro plan — non-Pro projects receive an empty config and the widget renders nothing.
Three display modes are supported:
- Banner — bottom slide-in (default)
- Modal — centered overlay with backdrop
- Inline — mounted inside a container you specify
Quick Start
Drop the script anywhere on your page:
<script src="https://seggwat.com/static/widgets/v1/seggwat-survey.js"
data-project-key="your-project-key"></script>The widget fetches live surveys from GET /api/v1/projects/{project_key}/surveys-config. For a survey to fire automatically, all of these must hold:
- The survey is in Live status.
- A trigger rule matches (URL pattern, custom event, or time-on-page).
- The visitor is past the survey's cooldown window (default 90 days).
- The per-survey sample-rate roll passes (default 100%).
- The org is on the Pro plan (required to publish the survey Live; Starter can only draft and preview).
Configuration Options
Required Attributes
data-project-keystringrequiredYour unique project identifier from the SeggWat dashboard.
Optional Attributes
data-modestringdefault:"banner"Display mode. Valid values: banner, modal, inline.
banner— fixed bottom slide-in, dismissiblemodal— centered overlay with backdropinline— mounted intodata-container, no backdrop
data-mode="modal"data-containerstringCSS selector for the container to mount the widget into. Required when data-mode="inline".
data-container="#survey-slot"data-survey-idstringWhen set, only the survey with this id is considered and its triggers are bypassed — the widget shows it as soon as it loads (still subject to cooldown / sample rate). Useful for embedding a specific survey on a static landing page.
data-match-targetstringdefault:"path"What URL-pattern triggers match against. Single-page apps (React, Vue, Next.js, Astro) that route without full page reloads are re-evaluated automatically on navigation; this controls what the patterns compare to.
path—window.location.pathname(default; correct for server-rendered and history-routed sites)hash— the hash treated as the route, so a pattern of/pricingmatches#/pricing(hash-routed SPAs)full—pathname + search + hash, so patterns can include query strings
data-match-target="hash"data-cooldown-daysnumberOverride the per-survey cooldown for all surveys returned by this embed. 0 disables the cooldown entirely. Intended for dev / QA.
data-global-cooldown-daysnumberOverride for the project-wide fatigue cap. Normally you set this once in the dashboard (Widgets → Surveys → "Don't show another survey for") and it applies to every embed automatically. This attribute is a dev/QA escape hatch that overrides the dashboard value for a single embed.
When in effect, no trigger-driven survey fires within this many days of any survey last being shown — independent of each survey's own cooldown. Forced shows (data-survey-id, SeggwatSurvey.show()) and lifecycle invitations bypass the cap but still reset its timer. 0 or unset disables it.
data-button-colorstringdefault:"#2563eb"Hex color used for primary buttons, selected states, and progress bar. Hex format only (e.g. #10b981).
data-languagestringdefault:"auto-detect"Language code for the widget UI chrome (Step labels, Next/Back/Submit/Skip). Supported: en, de, sv, fr. Falls back to English when unsupported. Per-survey copy overrides authored in the dashboard take precedence over these defaults.
data-show-powered-bybooleandefault:"true"Show or hide the "Powered by SeggWat" footer. Set to false, 0, or no to hide.
data-versionstringTrack responses against specific application versions (e.g. 1.2.3, v2.0.0-beta). Stored on linked Rating / Feedback / SurveyResponse rows.
data-api-urlstringdefault:"auto-detect"Override the API endpoint. Useful for self-hosted or staging environments.
Triggers (configured per survey)
Each survey has its own trigger configuration. The widget evaluates triggers on every survey returned by the config endpoint and shows the first one that matches (one survey at a time). When more than one survey matches the same URL, the most recently updated survey wins — editing or launching a survey is treated as the "make this take precedence" signal.
A trigger reads as a sentence with three parts:
Show when {the when conditions are met} on pages matching {the where patterns}, except on {the exclude patterns}.
- Where (
url_patterns) — a page gate. The survey can only fire on matching paths. Empty means "any page". - When (events, time-on-page, scroll depth, exit intent) — the firing moment. Empty means "as soon as the where gate is satisfied" (page load).
- Except (
exclude_url_patterns) — paths that always suppress the survey.
The where and when groups are AND'd together: the survey fires when a when condition is met while the current path satisfies the where gate. Within each group, a combinator (url_match / when_match) chooses any (OR, the default) or all (AND) — see Combining conditions.
URL path patterns (where)
Glob-style patterns matched against window.location.pathname by default (configurable via data-match-target for hash-routed SPAs or query strings):
*matches any sequence within a single path segment**matches across segments
url_match controls how multiple patterns combine — any (default) fires on any matching path; all requires every pattern to match. A survey with only a where gate (no when conditions) fires as soon as the user lands on a matching path.
In single-page apps the widget re-evaluates URL patterns on every client-side route change (pushState/replaceState, popstate, hashchange), so a survey targeting /pricing fires even when the user navigates there without a full page reload. The where gate is re-checked at fire time, so a when trigger armed on one page won't fire after the user navigates off the gated path.
Custom event names
Names the widget listens for. Fire them from your own code at meaningful milestones:
window.SeggwatSurvey.trigger("checkout_complete");
window.SeggwatSurvey.trigger("first_export");Time-on-page (when)
Show the survey after N seconds of foreground (visible) time on the page — time spent on a backgrounded tab doesn't count.
Scroll depth
Show the survey once the visitor scrolls past a percentage of the page (1–100). Useful for content and documentation pages where engagement correlates with scroll.
Exit intent
Show the survey when the pointer leaves the viewport toward the top of the window — the classic abandonment signal. Desktop only: the widget gates on a fine pointer, so it never fires on touch devices.
Exclude URL patterns
A suppression guard (not a "show" condition). Paths matching these glob patterns never fire the survey, even when another trigger condition matches. Takes precedence over every other condition and is re-checked on SPA navigation, so navigating into an excluded path suppresses an already-armed time/scroll/exit/event trigger.
Combining conditions
The where group and the when group are always AND'd together, and exclude_url_patterns always overrides. Two combinators control how conditions combine within a group:
| Field | Group | any (default) |
all |
|---|---|---|---|
url_match |
where | fire on any matching path | every pattern must match the path |
when_match |
when | the first met condition fires the survey | every condition must be met (each latches once satisfied) |
With when_match: all, exit-intent is desktop-only, so on touch devices it's dropped from the set rather than blocking the other conditions. A when trigger that completes off the where-gated path (or is sampled out) is not retried.
Behavior change (widget 1.4.0). Earlier widgets OR'd the URL pattern together with the time/scroll/exit/event conditions and fired immediately on a URL match. URL patterns are now a where gate that's AND'd with the when conditions. A survey set to "on /pricing and after 30s" now fires after 30s on /pricing, where it previously fired the instant the user reached /pricing.
Question Kinds
The widget renders any combination of these question shapes:
| Kind | Input | Notes |
|---|---|---|
open_text |
textarea | Honors max_chars and placeholder |
nps |
0–10 button row | Shows "Not likely" / "Very likely" labels |
scale |
min–max button row | Custom low_label / high_label shown beneath |
single_choice |
radio list | One option from a list |
multi_choice |
checkbox list | Multiple, with optional min / max cardinality |
boolean |
two-button toggle | Custom true_label / false_label |
Choice options can set allow_other_text: true. When the user picks that option, a free-text "Other:" input appears beneath the choices and is submitted as other_text on the answer.
Each question can also be marked required. The Next button stays disabled until the answer is non-empty; non-required questions show a Skip button.
Thank-You Actions
After the last question, the widget shows the server-resolved thank-you action:
- Message — headline + optional body text
- DiscountOffer — headline, coupon code (monospace, dashed border), CTA button. The widget stays open longer (12s) before auto-hiding to give the user time to copy the coupon
- Reactivate — headline + reactivate CTA (typically for win-back flows)
- Custom — arbitrary HTML (only org admins can author surveys, so this is trusted to the same extent as the survey definition)
Install with an AI assistant
Using Cursor, Claude Code, GitHub Copilot, or another AI coding assistant? Paste this prompt and it will add the widget for you. Replace YOUR_PROJECT_KEY with your project key from the dashboard.
Add the SeggWat survey widget to my site. It's a single script tag — no
package to install, no SDK to wire up.
<script src="https://seggwat.com/static/widgets/v1/seggwat-survey.js"
data-project-key="YOUR_PROJECT_KEY"></script>
Rules:
- Add the tag once in the shared root layout / template (e.g. Next.js root
layout, Astro base layout, the master <head> or before </body>), NOT
per-page. The widget pulls live survey definitions and targeting rules from
the server, so one global include covers the whole site.
- This one tag is the entire install. Don't add any other SeggWat scripts,
don't npm/yarn install anything, and don't build a framework wrapper for it.
- Leave data-project-key as YOUR_PROJECT_KEY — I'll swap in my real key.
- Which surveys fire, where, and when is authored in the SeggWat dashboard, so
don't add extra data-* attributes unless I ask.Examples
Banner (default)
<script src="https://seggwat.com/static/widgets/v1/seggwat-survey.js"
data-project-key="your-project-key"></script>Modal centered overlay
<script src="https://seggwat.com/static/widgets/v1/seggwat-survey.js"
data-project-key="your-project-key"
data-mode="modal"></script>Inline inside a specific container
<div id="survey-slot"></div>
<script src="https://seggwat.com/static/widgets/v1/seggwat-survey.js"
data-project-key="your-project-key"
data-mode="inline"
data-container="#survey-slot"></script>Force a specific survey, bypass its triggers
<script src="https://seggwat.com/static/widgets/v1/seggwat-survey.js"
data-project-key="your-project-key"
data-survey-id="65cf...e9"
data-mode="inline"
data-container="#exit-survey"></script>Dev / QA (bypass cooldown across all surveys)
<script src="https://seggwat.com/static/widgets/v1/seggwat-survey.js"
data-project-key="your-project-key"
data-cooldown-days="0"></script>JavaScript API
The widget exposes window.SeggwatSurvey for programmatic control:
Identify the user
SeggwatSurvey.setUser("user_12345");User IDs are opaque — max 255 chars, alphanumeric + hyphens + underscores. Never send PII.
Fire a custom event trigger
SeggwatSurvey.trigger("onboarding_done");Dispatches a seggwat:survey:<name> event on window. Any survey whose event_names contains that name will be shown (subject to cooldown + sample rate).
Show a specific survey manually
SeggwatSurvey.show("survey_id"); // respects cooldown
SeggwatSurvey.show("survey_id", { force: true }); // bypass cooldown (dev only)
SeggwatSurvey.hide();Inspect state
SeggwatSurvey.hasAnswered("survey_id"); // boolean — inside cooldown window?
SeggwatSurvey.surveys; // read-only snapshot of live surveysReset local state
Clears this visitor's per-survey cooldown / sample-roll stored in localStorage. Does not delete server-side data.
SeggwatSurvey.reset();Veto a survey before it appears
Right before any survey paints, the widget dispatches a cancelable seggwat:survey:beforeshow event on window. Call preventDefault() to suppress that survey — useful when your own modal or checkout flow is open. This fires on every show path (trigger, manual show(), and lifecycle invitation), so it's a single choke point.
window.addEventListener("seggwat:survey:beforeshow", (e) => {
if (document.querySelector(".my-checkout-modal.is-open")) {
e.preventDefault(); // don't stack a survey on top of checkout
}
// e.detail = { survey_id, invitation_token }
});When a lifecycle-invitation survey is vetoed, the widget moves on to the next pending invitation token.
Accessibility
- Modal surveys are a real focus trap: focus moves into the dialog on open,
Tab/Shift+Tabcycle within it, and focus is restored to the previously focused element on close. Escapedismisses banner and modal surveys (inline surveys have no dismiss affordance). A dismiss without engagement enters the cooldown; dismissing mid-answer does not, so the visitor can resume later.- Banner and inline surveys never steal focus, so they don't interrupt keyboard users mid-task.
How It Works
-
Script loads, auto-detects its base URL, bootstraps
seggwat-core.jsif not already present. -
Fetches the live surveys list from
GET /api/v1/projects/{project_key}/surveys-config(sorted most-recently-updated first). -
For each survey: if it has no when conditions, fires as soon as the where gate matches the path. Otherwise arms its when triggers (time-on-page visible-time only, scroll-depth, exit-intent, event-name listeners) and fires once they're satisfied per
when_match. SPA route changes re-run where matching. -
On fire: fires the cancelable
beforeshowhook, then checks the global fatigue cap, exclude patterns, the where gate (still matching?),localStoragecooldown, and sticky sample-rate roll. If all pass, displays the survey in the configured mode. -
User steps through one question per screen (Prev / Next), with required validation.
-
On submit, POSTs to
/api/v1/surveys/submitwithproject_key+survey_id+ answers. -
Server atomically writes: one
Ratingper measurable answer (NPS →nps, boolean →helpful, 1–N scale where N ≤ 10 →star; scales outside that range stay sidecar-only), one combinedFeedbackrow for all open-text answers (taggedsource: Survey), and oneSurveyResponsesidecar linking everything. -
Widget renders the resolved
thank_youaction and auto-hides after a short grace. -
Script loads, auto-detects its base URL, bootstraps
seggwat-core.jsif not already present. -
Fetches the live surveys list from
GET /api/v1/projects/{project_key}/surveys-config. -
For each survey: if it has no when conditions, fires as soon as the where gate matches the path. Otherwise arms its when triggers (time-on-page, scroll-depth, exit-intent, event-name listeners) and fires once they're satisfied per
when_match. -
On fire: checks the where gate,
localStoragecooldown + sticky sample-rate roll. If all pass, displays the survey in the configured mode. -
User steps through one question per screen (Prev / Next), with required validation.
-
On submit, POSTs to
/api/v1/surveys/submitwithproject_key+survey_id+ answers. -
Server atomically writes: one
Ratingper measurable answer (NPS →nps, boolean →helpful, 1–N scale where N ≤ 10 →star; scales outside that range stay sidecar-only), one combinedFeedbackrow for all open-text answers (taggedsource: Survey), and oneSurveyResponsesidecar linking everything. -
Widget renders the resolved
thank_youaction and auto-hides after a short grace.
API Endpoints
Submit a response
Payload (client-triggered path):
{
"project_key": "uuid",
"survey_id": "65cf...e9",
"answers": [
{ "question_id": "q1", "value": { "type": "nps", "value": 9 } },
{ "question_id": "q2", "value": { "type": "text", "value": "Loved the trial" } }
],
"completed": true,
"path": "/dashboard",
"version": "1.2.3",
"submitted_by": "user_12345",
"locale": "en"
}Response (201 Created):
{
"response_id": "...",
"feedback_id": "...",
"rating_ids": ["...", "..."],
"thank_you": {
"type": "message",
"headline": "Thanks!",
"body": "Your response has been recorded."
}
}feedback_id is only present when at least one open-text answer was given. thank_you is null if the visitor didn't qualify (e.g. partial submission with show_to: completers).
Fetch the widget config
Public, no auth. Returns the live, ClientSide-triggered surveys for the project plus their triggers, cooldown, sample rate, copy overrides, and thank-you actions. Returns an empty list when the project is not on Pro.
Track impressions and dismissals
Public, no auth. Batches shown / dismissed events from the widget so the dashboard can show response and dismissal rates next to raw submission counts. The widget fires these automatically — you don't need to call this endpoint yourself.
Payload:
{
"project_key": "uuid",
"events": [
{ "survey_id": "65cf...e9", "kind": "shown" },
{ "survey_id": "65cf...e9", "kind": "dismissed" }
]
}Response (202 Accepted):
{ "accepted": 2 }The third event kind, submitted, is incremented server-side from POST /surveys/submit. Don't send it from the widget.
Privacy and cookies
The Survey widget is built to keep customer pages free of consent-banner surface area. Concretely:
- No cookies are set. Per-survey cooldown / sample-rate state is stored in
localStorage, which under EU guidance is considered "strictly necessary" for delivering the feature the visitor is interacting with. - No tracking identifiers leave the browser. The widget never sends an IP, user agent, fingerprint, or anonymous visitor ID as part of an event. Server logs naturally capture IP for short-lived rate limiting, but that data is never persisted into the analytics counters.
- Event counters are pure aggregates. Impressions and dismissals are stored only as
(project, survey, day, event_kind, count)rows. There is no way to reconstruct who saw which survey from that data, which means the counters are not personal data under the GDPR. submitted_byis opt-in. If you callSeggwatSurvey.setUser("...")or passdata-user-id, that identifier rides along on the submitted response. It does not appear on impression / dismissal events.
If your privacy policy already covers SeggWat for feedback / ratings, you typically do not need to add a separate disclosure for survey event counters — the same "non-identifying aggregate analytics" framing applies.
Data Model
Each submission writes up to three correlated row types:
- Rating — one row per measurable answer, feeding the existing aggregation pipelines:
- NPS answer →
type: "nps"(0–10 with band stats) - Boolean answer →
type: "helpful"(true/false) - Scale answer with
min: 1andmax ≤ 10→type: "star"withmax_stars = max(e.g. CSAT 1–5, CES 1–7) - Scales outside that range (0-based, > 10) are preserved on the sidecar only — no Rating row is created
- Every Rating carries the same
path,version, andsubmitted_byas the submission so it shows up under the standard stats filters
- NPS answer →
- Feedback — a single combined row for all open-text answers in the submission, formatted as
**prompt**\nanswer\n\n**prompt**\nanswer. Taggedsource: "Survey". Flows through the regular triage queue. - SurveyResponse sidecar — links every Rating + Feedback row, denormalizes every answer (including its
prompt_snapshot), and carriescompleted,locale,path,version,submitted_by.
Question identity is preserved via stable string ids on each SurveyQuestion. Reordering, renaming, or rewording questions in the dashboard later doesn't corrupt analytics — historical responses keep referencing the original question_id and carry their own prompt snapshot.
Best Practices
Use Cases
Activation Pulse
Fire SeggwatSurvey.trigger("activation_complete") after the user completes onboarding. A 3-question survey (clarity / value / blockers) captures impressions while they're fresh.
Pricing-Page Intent
URL trigger on /pricing. Ask what they're evaluating and the biggest blocker. Pair with a DiscountOffer thank-you on completion.
Feature Feedback
Custom-event trigger after the user uses a new feature N times. Tight, focused: clarity / usefulness / what's missing.
Pre-Cancel Save
URL trigger on /account/cancel. Open-text "what's not working" + a reactivate CTA in the thank-you. Catches churn intent before it lands.
