FastPoll

FastPoll gives you a quick read on product questions with low-friction single- or multi-choice polls.

@sensefolks/fastpoll WCAG 2.1 AA <12KB gzipped

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:

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:

bash
npm install @sensefolks/fastpoll

Framework Integration

HTML / Vanilla JS

html
<sf-fastpoll 
  survey-key="your-survey-uuid" 
  completion-message="Thank you for your feedback!"
  enable-events="true">
</sf-fastpoll>

React

jsx
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:

typescript
// 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

vue
<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:

typescript
// 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:

typescript
// 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:

typescript
// 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

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:

jsx
'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:

  1. Poll Step — Single or multiple choice question
  2. Follow-up Step (optional) — Open-ended question triggered by specific choices
  3. Respondent Details (optional) — Collect name, email, or custom fields
  4. 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

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

jsx
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:

vue
<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
css
/* 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-containerOuter wrapper for the entire survey
stepWrapper for each survey step
poll-stepWrapper for the poll step
follow-up-stepWrapper for the follow-up step
respondent-details-stepWrapper for the respondent details step
completion-stepWrapper for the completion/thank-you screen
headingAll heading elements
poll-headingThe poll question heading
follow-up-headingThe follow-up question heading
respondent-details-headingThe respondent details heading
completion-headingThe completion screen heading
choices-containerWrapper for all choice options
choice-optionA single choice option label
choice-labelThe text label for a choice option
radio-inputA radio input element
checkbox-inputA checkbox input element
followup-input-containerWrapper for the follow-up textarea
textareaThe follow-up textarea element
followup-textareaThe follow-up textarea (alias)
other-choice-inputInput field for the "Other" choice option
form-containerWrapper for respondent details form fields
form-fieldA single form field wrapper
form-labelA form field label
form-inputA text/email/number input field
inputInput field (alias for form-input)
form-selectA dropdown select field
hcaptcha-containerWrapper for the hCaptcha widget (when enabled)
selectDropdown select (alias for form-select)
required-indicatorThe asterisk indicator for required fields
radio-groupWrapper for a radio button group
radio-optionA single radio option row
radio-labelA radio option label
checkbox-groupWrapper for a checkbox group
checkbox-optionA single checkbox option row
checkbox-labelA checkbox option label
button-containerWrapper for navigation buttons
buttonAll buttons
back-buttonThe "Back" navigation button
next-buttonThe "Next" navigation button
submit-buttonThe "Submit" action button
retry-buttonThe retry button shown on error
error-containerWrapper for error state display
error-messageError message text
messageAll message elements (loading, error)
loading-messageLoading state text
announcementsARIA live region for screen reader announcements
brandingSenseFolks branding footer
branding-linkThe anchor link within the branding footer
branding-logoThe SVG logo within the branding footer
css
/* 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:

css
/* 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-key is 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 isCustomElement in 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
Chrome88+Full support
Firefox85+Full support
Safari14+Full support
Edge88+Full support
IE11 SupportedES5 build with polyfills