@opengovsg/confetti
v0.9.0
Published
Widgets for Confetti
Keywords
Readme
@opengovsg/confetti
Embeddable React survey widget for collecting user feedback.
Installation
# npm
npm install @opengovsg/confetti
# yarn
yarn add @opengovsg/confetti
# pnpm
pnpm add @opengovsg/confettiPeer Dependencies
This package requires React 18.2.0+ or React 19:
npm install react@^18.2.0Quick Start
- Add the widget to your page:
import { PopoverConfetti } from '@opengovsg/confetti'
function App() {
return (
<div style={{ position: 'fixed', bottom: '1rem', right: '1rem' }}>
<PopoverConfetti
surveyId="your-survey-id"
publishableKey="your-publishable-key"
metadata={{}}
/>
</div>
)
}The template components load their script bundle and stylesheet from the
Confetti CDN at runtime, so there's no CSS to import — including the
host-rendered trigger button, which TriggerPopoverConfetti styles itself. (The
lower-level @opengovsg/confetti/components primitives and
the @opengovsg/confetti/static opt-out surface render in your own bundle and
still require import '@opengovsg/confetti/confetti.css'.)
Note: You'll need the survey ID and publishable key from your Confetti dashboard. Make sure to whitelist your domain in your team settings.
Analytics metadata
Analytics metadata collection is disabled by default. Set
autoInjectAnalyticsMetadata to true to include identifiers exposed by
PostHog and Datadog RUM on the host page with each response:
<PopoverConfetti
surveyId="your-survey-id"
publishableKey="your-publishable-key"
autoInjectAnalyticsMetadata
/>The widget reads these SDKs from window.posthog and window.DD_RUM, and it
never bundles either SDK. @datadog/browser-rum assigns window.DD_RUM for
you as soon as it is imported, so Datadog needs no extra setup.
PostHog is different — you have to expose it yourself. It
documents
that most modern apps won't have window.posthog, so assign it wherever you
initialise PostHog:
import posthog from 'posthog-js'
posthog.init('<your-project-token>', { api_host: '<your-api-host>' })
window.posthog = posthogAny identifier whose SDK is missing is simply skipped, so a partial setup still submits the rest. These keys are collected when available:
| Key | Source |
| ------------------------------------------------------------------------ | ------------------------------------ |
| posthog_distinct_id | posthog.get_distinct_id() |
| posthog_session_id | posthog.get_session_id() |
| datadog_rum_session_id | DD_RUM.getInternalContext() |
| datadog_rum_user | DD_RUM.getUser(), JSON stringified |
| datadog_rum_user_id, datadog_rum_user_email, datadog_rum_user_name | DD_RUM.getUser() standard fields |
Explicit values passed through metadata always take precedence over
automatically collected values.
Use the Datadog session ID to find the session in Datadog Sessions Explorer
with @session.id:<datadog_rum_session_id>.
- Add Confetti to your CSP headers. The widget loads its script and stylesheet from the CDN and calls the API, so allowlist all three:
script-src https://confetti.gov.sg;
style-src https://confetti.gov.sg;
connect-src https://confetti.gov.sg;Proxying requests
Every template component accepts an optional proxyUrl. When set, the widget
sends its API requests to {proxyUrl}/api/v1/cfti/... instead of
https://confetti.gov.sg, and the CDN-backed shells also load their script
and stylesheet from {proxyUrl}/widget/v1/.... Use this to route widget
traffic through your own origin, for example when your CSP disallows third
party hosts.
Confetti Cloud teams can provision a managed proxy from Team settings. Create the DNS-only CNAME shown there and wait for it to become live, then pass the resulting URL. If the hostname is in Cloudflare DNS, disable the orange cloud so Proxy status is DNS only — see the managed proxies guide.
<PopoverConfetti
proxyUrl="https://c.example.gov.sg"
surveyId="..."
publishableKey="cfti_pk_..."
/>When using a managed proxy, use that origin for all three CSP directives shown
above instead of https://confetti.gov.sg.
Template Components
Choose the component that best fits your use case:
| Component | Use Case | Behavior |
| ------------------------ | --------------------------------- | ------------------------------------------------------------------------------------- |
| EmbeddedConfetti | Inline surveys, feedback sections | Shows questions in an inline manner with a submit button |
| ModalConfetti | Interruptive, centered surveys | Centered modal overlay, step-by-step questions, auto-submits |
| PopoverConfetti | Floating feedback widgets | Dismissible popover, step-by-step questions, auto-submits |
| TriggerPopoverConfetti | Feedback launcher button | Renders a trigger button that opens a dismissible popover survey on click; reopenable |
| StepperConfetti | Guided surveys | One question at a time, auto-submits after last question |
Locale
Requires @opengovsg/confetti 0.8.2 or later. Older versions keep the
English survey — they do not fail to load.
Pass locale to show a translated survey. Author translations in the Confetti
dashboard (English is always the source; Chinese, Malay, and Tamil are
optional). Untranslated fields fall back to English.
The widget does not read the browser language on its own. Use
useResolveLocale to map a BCP-47 tag (or the browser's language
preferences) to 'en' | 'zh' | 'ms' | 'ta':
import { PopoverConfetti, useResolveLocale } from '@opengovsg/confetti'
function Feedback() {
const locale = useResolveLocale()
return (
<PopoverConfetti
surveyId="your-survey-id"
publishableKey="your-publishable-key"
locale={locale}
/>
)
}Pass preferred when the host app already knows the locale (for example
next-intl). Pass explicit for an override that wins even when it does not
match a Confetti locale — including an empty string, which falls back to
English instead of preferred:
const locale = useResolveLocale({
preferred: i18n.locale,
explicit: searchParams.get('locale'),
})To always show one language, pass locale directly and skip the hook:
<PopoverConfetti
surveyId="your-survey-id"
publishableKey="your-publishable-key"
locale="zh"
/>| Value | Language |
| ----- | ----------------- |
| en | English (default) |
| zh | 中文 |
| ms | Bahasa Melayu |
| ta | தமிழ் |
Widget chrome you did not customise — Skip, Next, Submit, Back, Close, Other,
placeholders, and validation errors — still localises for the given locale.
Omit locale, or pass 'en', to keep the English survey.
The same prop is accepted on the static components and
on the building-block primitives. Import
useResolveLocale from @opengovsg/confetti in those setups too. See the
translations guide.
Static Components
The @opengovsg/confetti/static subpath ships self-contained builds of the
same template components (EmbeddedConfetti, ModalConfetti,
PopoverConfetti, StepperConfetti) plus the ConfettiTrigger composition
helper. They render entirely from your own bundle with no runtime CDN
dependency, so survey rendering updates only reach your site when you upgrade
the package.
import '@opengovsg/confetti/confetti.css'
import { PopoverConfetti } from '@opengovsg/confetti/static'Unlike the CDN-backed shells, the static components require the stylesheet import shown above.
Visibility Hooks
Control when your survey appears with these hooks:
useVisibleAfterDelay
Show the survey after a delay:
import { useVisibleAfterDelay } from '@opengovsg/confetti'
const { isVisible } = useVisibleAfterDelay({ delay: 5000 }) // 5 secondsuseVisibleAfterScroll
Show the survey after scrolling a certain distance:
import { useVisibleAfterScroll } from '@opengovsg/confetti'
const { isVisible } = useVisibleAfterScroll({ threshold: 500 }) // 500pxuseVisibleAfterPageVisits
Show the survey once the visitor has loaded the current page a given number of times (persisted across sessions via localStorage):
import { useVisibleAfterPageVisits } from '@opengovsg/confetti'
const { isVisible, reset } = useVisibleAfterPageVisits({
visits: 3,
respondent: 'optional-respondent-identifier',
})useVisibleAfterSessionPageVisits
Same as above, but the visit count is scoped to the current browser session via sessionStorage:
import { useVisibleAfterSessionPageVisits } from '@opengovsg/confetti'
const { isVisible, reset } = useVisibleAfterSessionPageVisits({ visits: 3 })useRespondentState
Read a respondent's response and dismissal times without mounting a template.
respondent is required (this hook does not generate an anonymous id).
import { useRespondentState } from '@opengovsg/confetti'
const respondentState = useRespondentState({
surveyId: '<your-survey-id>',
publishableKey: '<your-publishable-key>',
respondent: '<user-id>',
})Building Blocks
The lower-level primitives that power the template components are published from the @opengovsg/confetti/components subpath. Reach for these to compose your own survey UI.
import {
ConfettiController,
ConfettiProvider,
SurveyContext,
useSurvey,
} from '@opengovsg/confetti/components'Unlike the template components, these primitives render in your own bundle, so import the stylesheet once in your app entry point:
import '@opengovsg/confetti/confetti.css'