FastPoll
FastPoll gives you a quick read on product questions with low-friction single- or multi-choice polls.
How It Works
FastPoll is a single- or multi-choice poll you can embed anywhere. Define the question and choices in the dashboard. You can optionally add follow-up text questions and respondent detail fields.
The flow is: poll question, optional follow-up, optional respondent details, then completion.
When to Use FastPoll
Embed FastPoll wherever you need a quick signal:
- Blog posts and tutorials — "Was this helpful?"
- Feature announcements — "Interested in this feature?"
- Release notes — "How do you feel about this update?"
- Community pages — "What topic should we cover next?"
- Product pages — "Which feature matters most to you?"
- Onboarding flows — "What's your primary use case?"
Installation
CDN (Recommended)
Add directly to your HTML:
<!-- Modern browsers (ES modules) -->
<script type="module" src="https://unpkg.com/@sensefolks/fastpoll/dist/sf-fastpoll/sf-fastpoll.esm.js"></script>
<!-- Legacy browsers -->
<script nomodule src="https://unpkg.com/@sensefolks/fastpoll/dist/sf-fastpoll/sf-fastpoll.js"></script> NPM
For projects with a build system:
npm install @sensefolks/fastpoll Framework Integration
HTML / Vanilla JS
<sf-fastpoll
survey-key="your-survey-uuid"
completion-message="Thank you for your feedback!"
enable-events="true">
</sf-fastpoll> React
import '@sensefolks/fastpoll';
function App() {
return (
<sf-fastpoll
survey-key="your-survey-uuid"
completion-message="Thank you!">
</sf-fastpoll>
);
} For TypeScript projects, add type declarations:
// sf-fastpoll.d.ts - Add to your project
declare namespace JSX {
interface IntrinsicElements {
'sf-fastpoll': React.DetailedHTMLProps<
React.HTMLAttributes<HTMLElement> & {
'survey-key': string;
'completion-message'?: string;
'enable-events'?: boolean;
},
HTMLElement
>;
}
} Vue 3
<template>
<sf-fastpoll
survey-key="your-survey-uuid"
completion-message="Thank you!">
</sf-fastpoll>
</template>
<script setup>
import '@sensefolks/fastpoll';
</script> Configure Vue to recognize custom elements:
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [
vue({
template: {
compilerOptions: {
// Treat all tags starting with sf- as custom elements
isCustomElement: (tag) => tag.startsWith('sf-')
}
}
})
]
}) Angular
Add CUSTOM_ELEMENTS_SCHEMA to your module:
// app.module.ts
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import '@sensefolks/fastpoll';
@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
// ... rest of module config
})
export class AppModule {} Use in your component:
// survey.component.ts
import { Component } from '@angular/core';
import '@sensefolks/fastpoll';
@Component({
selector: 'app-survey',
template: `
<sf-fastpoll
[attr.survey-key]="surveyKey"
completion-message="Thanks for voting!">
</sf-fastpoll>
`
})
export class SurveyComponent {
surveyKey = 'your-survey-uuid';
} Svelte
<script>
import '@sensefolks/fastpoll';
export let surveyKey;
</script>
<sf-fastpoll
survey-key={surveyKey}
completion-message="Thank you!">
</sf-fastpoll> Next.js
Use dynamic import for client-side rendering:
'use client';
import { useEffect } from 'react';
export default function Survey({ surveyKey }) {
useEffect(() => {
// Dynamic import for client-side only
import('@sensefolks/fastpoll');
}, []);
return (
<sf-fastpoll
survey-key={surveyKey}
completion-message="Thank you!">
</sf-fastpoll>
);
} API Reference
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
surveyKey | survey-key | string | — | Required. UUID of the survey from your dashboard |
completionMessage | completion-message | string | 'Thank you for your response!' | Message displayed after successful submission |
enableEvents | enable-events | boolean | true | Whether to emit custom events (sfReady, sfStepChange, sfChoiceSelect, sfSubmit, sfError) |
Survey Flow
FastPoll supports a multi-step flow configured in your dashboard:
- Poll Step — Single or multiple choice question
- Follow-up Step (optional) — Open-ended question triggered by specific choices
- Respondent Details (optional) — Collect name, email, or custom fields
- Completion — Thank you message
Response Data
Each submission automatically captures:
- Selected choices and follow-up responses
- Respondent details (if configured)
- Completion time in seconds
- User agent, timezone, and screen resolution
Custom Events
FastPoll emits custom events so you can respond to user interactions and track the survey lifecycle.
Events Reference
| Event | Description | Detail Properties |
|---|---|---|
sfReady | Survey loaded and ready to display | surveyKey, question, choiceType, choiceCount |
sfChoiceSelect | User selected or deselected a choice | surveyKey, selectedChoices, choiceType, lastAction, lastChoice |
sfStepChange | User navigated between survey steps | surveyKey, previousStep, currentStep, currentStepIndex |
sfSubmit | Survey submitted successfully | surveyKey, selectedChoices, followUpResponse, completionTimeSeconds |
sfError | Error occurred (load, submit, or validation) | surveyKey, errorType, errorMessage |
Vanilla JavaScript
const poll = document.querySelector('sf-fastpoll');
// Survey loaded and ready
poll.addEventListener('sfReady', (e) => {
console.log('Survey ready:', e.detail);
// { surveyKey, question, choiceType, choiceCount }
});
// User selected/deselected a choice
poll.addEventListener('sfChoiceSelect', (e) => {
console.log('Choice selected:', e.detail);
// { surveyKey, selectedChoices, choiceType, lastAction, lastChoice }
});
// User navigated between steps
poll.addEventListener('sfStepChange', (e) => {
console.log('Step changed:', e.detail);
// { surveyKey, previousStep, currentStep, currentStepIndex }
});
// Survey submitted successfully
poll.addEventListener('sfSubmit', (e) => {
console.log('Survey submitted:', e.detail);
// { surveyKey, selectedChoices, followUpResponse, completionTimeSeconds }
});
// Error occurred
poll.addEventListener('sfError', (e) => {
console.error('Survey error:', e.detail);
// { surveyKey, errorType, errorMessage }
}); React
import { useEffect, useRef } from 'react';
function SurveyWithEvents({ surveyKey }) {
const pollRef = useRef(null);
useEffect(() => {
import('@sensefolks/fastpoll');
const poll = pollRef.current;
if (!poll) return;
const handleReady = (e) => {
console.log('Survey ready:', e.detail);
};
const handleSubmit = (e) => {
console.log('Submitted:', e.detail);
// Track conversion, show thank you modal, etc.
};
const handleChoiceSelect = (e) => {
// Track user engagement
analytics.track('poll_choice_selected', e.detail);
};
poll.addEventListener('sfReady', handleReady);
poll.addEventListener('sfSubmit', handleSubmit);
poll.addEventListener('sfChoiceSelect', handleChoiceSelect);
return () => {
poll.removeEventListener('sfReady', handleReady);
poll.removeEventListener('sfSubmit', handleSubmit);
poll.removeEventListener('sfChoiceSelect', handleChoiceSelect);
};
}, []);
return (
<sf-fastpoll
ref={pollRef}
survey-key={surveyKey}
completion-message="Thanks!">
</sf-fastpoll>
);
} Vue 3
Vue automatically converts custom events to kebab-case listeners:
<template>
<sf-fastpoll
ref="pollRef"
:survey-key="surveyKey"
completion-message="Thanks!"
@sfReady="onReady"
@sfSubmit="onSubmit"
@sfChoiceSelect="onChoiceSelect"
@sfStepChange="onStepChange"
@sfError="onError">
</sf-fastpoll>
</template>
<script setup>
import { onMounted } from 'vue';
const props = defineProps(['surveyKey']);
onMounted(() => {
import('@sensefolks/fastpoll');
});
function onReady(e) {
console.log('Survey ready:', e.detail);
}
function onSubmit(e) {
console.log('Submitted:', e.detail);
}
function onChoiceSelect(e) {
console.log('Choice selected:', e.detail);
}
function onStepChange(e) {
console.log('Step changed:', e.detail);
}
function onError(e) {
console.error('Error:', e.detail);
}
</script> Common Use Cases
- Analytics tracking — Track choice selections and submissions
- Conditional UI — Show or hide elements based on survey state
- Error handling — Display custom error messages or retry logic
- Progress tracking — Update progress indicators on step changes
- Conversion tracking — Fire conversion pixels on successful submit
CSS Custom Properties
Override these custom properties to theme the component:
| Property | Default | Description |
|---|---|---|
--sf-primary | #005fcc | Primary accent color |
--sf-primary-hover | #0047a3 | Primary color hover state |
--sf-text-primary | #111827 | Primary text color |
--sf-text-secondary | #6b7280 | Secondary/muted text color |
--sf-error-color | #dc2626 | Error state color |
--sf-card-bg | #ffffff | Card/container background |
--sf-card-border | #d1d5db | Card/container border color |
--sf-card-radius | 8px | Card/container border radius |
--sf-button-radius | 6px | Button border radius |
--sf-transition | 150ms ease | Default transition timing |
--sf-followup-border | #d1d5db | Border color for the follow-up textarea |
--sf-choice-hover-bg | #f3f4f6 | Background color on choice option hover |
/* Override theme tokens on the component */
sf-fastpoll {
--sf-primary: #7c3aed;
--sf-primary-hover: #6d28d9;
--sf-card-radius: 12px;
--sf-button-radius: 8px;
--sf-choice-hover-bg: #f5f3ff;
--sf-followup-border: #a78bfa;
} CSS Parts
Style individual elements inside the shadow DOM using ::part():
| Part | Description |
|---|---|
survey-container | Outer wrapper for the entire survey |
step | Wrapper for each survey step |
poll-step | Wrapper for the poll step |
follow-up-step | Wrapper for the follow-up step |
respondent-details-step | Wrapper for the respondent details step |
completion-step | Wrapper for the completion/thank-you screen |
heading | All heading elements |
poll-heading | The poll question heading |
follow-up-heading | The follow-up question heading |
respondent-details-heading | The respondent details heading |
completion-heading | The completion screen heading |
choices-container | Wrapper for all choice options |
choice-option | A single choice option label |
choice-label | The text label for a choice option |
radio-input | A radio input element |
checkbox-input | A checkbox input element |
followup-input-container | Wrapper for the follow-up textarea |
textarea | The follow-up textarea element |
followup-textarea | The follow-up textarea (alias) |
other-choice-input | Input field for the "Other" choice option |
form-container | Wrapper for respondent details form fields |
form-field | A single form field wrapper |
form-label | A form field label |
form-input | A text/email/number input field |
input | Input field (alias for form-input) |
form-select | A dropdown select field |
hcaptcha-container | Wrapper for the hCaptcha widget (when enabled) |
select | Dropdown select (alias for form-select) |
required-indicator | The asterisk indicator for required fields |
radio-group | Wrapper for a radio button group |
radio-option | A single radio option row |
radio-label | A radio option label |
checkbox-group | Wrapper for a checkbox group |
checkbox-option | A single checkbox option row |
checkbox-label | A checkbox option label |
button-container | Wrapper for navigation buttons |
button | All buttons |
back-button | The "Back" navigation button |
next-button | The "Next" navigation button |
submit-button | The "Submit" action button |
retry-button | The retry button shown on error |
error-container | Wrapper for error state display |
error-message | Error message text |
message | All message elements (loading, error) |
loading-message | Loading state text |
announcements | ARIA live region for screen reader announcements |
branding | SenseFolks branding footer |
branding-link | The anchor link within the branding footer |
branding-logo | The SVG logo within the branding footer |
/* Container & Layout */
sf-fastpoll::part(survey-container) { }
sf-fastpoll::part(step) { }
sf-fastpoll::part(poll-step) { }
sf-fastpoll::part(follow-up-step) { }
sf-fastpoll::part(respondent-details-step) { }
sf-fastpoll::part(completion-step) { }
/* Headings */
sf-fastpoll::part(heading) { }
sf-fastpoll::part(poll-heading) { }
sf-fastpoll::part(follow-up-heading) { }
sf-fastpoll::part(respondent-details-heading) { }
sf-fastpoll::part(completion-heading) { }
/* Poll Choices */
sf-fastpoll::part(choices-container) { }
sf-fastpoll::part(choice-option) { }
sf-fastpoll::part(choice-label) { }
sf-fastpoll::part(radio-input) { }
sf-fastpoll::part(checkbox-input) { }
/* Follow-up */
sf-fastpoll::part(followup-input-container) { }
sf-fastpoll::part(textarea) { }
sf-fastpoll::part(followup-textarea) { }
sf-fastpoll::part(other-choice-input) { }
/* Form Fields */
sf-fastpoll::part(form-container) { }
sf-fastpoll::part(form-field) { }
sf-fastpoll::part(form-label) { }
sf-fastpoll::part(form-input) { }
sf-fastpoll::part(input) { }
sf-fastpoll::part(select) { }
sf-fastpoll::part(form-select) { }
sf-fastpoll::part(hcaptcha-container) { }
sf-fastpoll::part(required-indicator) { }
/* Radio & Checkbox Groups */
sf-fastpoll::part(radio-group) { }
sf-fastpoll::part(radio-option) { }
sf-fastpoll::part(radio-label) { }
sf-fastpoll::part(checkbox-group) { }
sf-fastpoll::part(checkbox-option) { }
sf-fastpoll::part(checkbox-label) { }
/* Buttons */
sf-fastpoll::part(button-container) { }
sf-fastpoll::part(button) { }
sf-fastpoll::part(next-button) { }
sf-fastpoll::part(back-button) { }
sf-fastpoll::part(submit-button) { }
sf-fastpoll::part(retry-button) { }
/* Messages & States */
sf-fastpoll::part(message) { }
sf-fastpoll::part(error-message) { }
sf-fastpoll::part(loading-message) { }
sf-fastpoll::part(error-container) { }
sf-fastpoll::part(announcements) { }
/* Branding */
sf-fastpoll::part(branding) { }
sf-fastpoll::part(branding-link) { }
sf-fastpoll::part(branding-logo) { } Styling Example
Combine custom properties and ::part() for full control:
/* Theme with CSS custom properties */
sf-fastpoll {
--sf-primary: #7c3aed;
--sf-primary-hover: #6d28d9;
--sf-card-radius: 12px;
--sf-button-radius: 8px;
--sf-choice-hover-bg: #f5f3ff;
}
/* Fine-tune individual elements with ::part() */
sf-fastpoll::part(survey-container) {
max-width: 480px;
padding: 24px;
border: 1px solid var(--sf-card-border);
border-radius: var(--sf-card-radius);
}
sf-fastpoll::part(poll-heading) {
font-size: 1.25rem;
font-weight: 600;
color: var(--sf-text-primary);
margin-bottom: 1rem;
}
sf-fastpoll::part(choice-option) {
padding: 0.75rem 1rem;
border-radius: var(--sf-card-radius);
transition: background-color var(--sf-transition);
}
sf-fastpoll::part(submit-button) {
background-color: var(--sf-primary);
color: #ffffff;
border: none;
border-radius: var(--sf-button-radius);
padding: 10px 24px;
cursor: pointer;
}
sf-fastpoll::part(submit-button):hover {
background-color: var(--sf-primary-hover);
}
sf-fastpoll::part(followup-textarea) {
border-color: var(--sf-followup-border);
border-radius: var(--sf-button-radius);
} Accessibility
FastPoll is built with accessibility as a core feature:
- Keyboard Navigation — Tab through options, Enter/Space to select
- Screen Readers — ARIA labels, live regions for dynamic updates
- Focus Management — Visible focus indicators, logical tab order
- High Contrast — Works with Windows High Contrast Mode
- Reduced Motion — Respects
prefers-reduced-motion - Form Validation — Accessible error messages with
aria-invalid
Troubleshooting
Component not rendering
-
Verify the
survey-keyis a valid UUID from your dashboard - Check browser console for network errors
- Make sure the script tag uses
type="module"
TypeScript errors in React/Vue
- Add the type declarations shown in the React section above
-
For Vue, configure
isCustomElementin your build config
Styles not applying
-
Use
::part()selectors. Regular CSS selectors won't penetrate Shadow DOM - Check that part names match exactly (case-sensitive)
Survey shows "invalid public key"
- Copy the survey key from your dashboard's Embed tab
- Make sure the survey is published and active
Browser Support
| Browser | Version | Notes |
|---|---|---|
| Chrome | 88+ | Full support |
| Firefox | 85+ | Full support |
| Safari | 14+ | Full support |
| Edge | 88+ | Full support |
| IE11 | Supported | ES5 build with polyfills |