FastPoll

FastPoll asks one focused single- or multiple-choice question, with optional follow-up text and respondent fields. Use it for a directional signal that is quick to set up and answer.

@sensefolks/fastpollWCAG 2.1 AA<12KB gzipped

What FastPoll Records

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.

Answers and progress stay only in the mounted component. Reloading the page starts a new blank response.

When FastPoll Is the Right Survey

Use FastPoll when a short set of answer options can move one decision forward.

  • 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
<script type="module" src="https://unpkg.com/@sensefolks/[email protected]/dist/sf-fastpoll/sf-fastpoll.esm.js"></script>

NPM

For projects with a build system:

bash
npm install @sensefolks/fastpoll

Embed FastPoll

HTML / Vanilla JS

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

React + Next.js

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
    >;
  }
}

Next.js Client Component

Load the package after the client component mounts:

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>
  );
}

Vue + Nuxt

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-')
        }
      }
    })
  ]
})

For Nuxt SSR routes, register SenseFolks in a .client.tsplugin and render inside <ClientOnly>.

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>

For Astro and broader framework paths, use theEmbedding Tutorial.

API Reference

Properties

PropertyAttributeTypeDefaultDescription
surveyKeysurvey-keystringRequired. UUID of the survey from your dashboard
completionMessagecompletion-messagestring'Thank you for your response!'Message displayed after successful submission
thankYouMessagethank-you-messagestringOverrides the saved survey thank-you message
sessionDataRecord<string, string | number | boolean>{}Complete typed Session Data matching every field in the schema defined during survey creation
enableEventsenable-eventsbooleantrueWhether 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. Survey Fields (optional) — Ask for name, email, or other configured fields
  4. Completion — Thank you message

Response Data

Each submission automatically captures:

  • Selected choices and follow-up responses
  • Survey Field values and Session Data (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

EventDescriptionDetail Properties
sfReadySurvey loaded and ready to displaysurveyKey, question, choiceType, choiceCount
sfChoiceSelectUser selected or deselected a choicesurveyKey, selectedChoices, choiceType, lastAction, lastChoice
sfStepChangeUser navigated between survey stepssurveyKey, previousStep, currentStep, currentStepIndex
sfSubmitSurvey submitted successfullysurveyKey, selectedChoices, followUpResponse, completionTimeSeconds
sfErrorError 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 and diagnostics
  • 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:

PropertyDefaultDescription
--sf-primary#005fccPrimary accent color
--sf-primary-hover#0047a3Primary color hover state
--sf-text-primary#111827Primary text color
--sf-text-secondary#6b7280Secondary/muted text color
--sf-error-color#dc2626Error state color
--sf-card-bg#ffffffCard/container background
--sf-card-border#d1d5dbCard/container border color
--sf-card-radius8pxCard/container border radius
--sf-button-radius6pxButton border radius
--sf-transition150ms easeDefault transition timing
--sf-progress-bg#e5e7ebProgress track color
--sf-progress-fill#005fccProgress fill color
--sf-followup-border#d1d5dbBorder color for the follow-up textarea
--sf-choice-hover-bg#f3f4f6Background color on choice option hover
css
/* Override theme tokens on the component */
sf-fastpoll {
  --sf-primary: #7c3aed;
  --sf-primary-hover: #6d28d9;
  --sf-button-text: #ffffff;
  --sf-error-text: #991b1b;
  --sf-error-bg: #fef2f2;
  --sf-error-border: #fecaca;
  --sf-card-radius: 12px;
  --sf-button-radius: 8px;
  --sf-progress-bg: #ede9fe;
  --sf-progress-fill: #7c3aed;
  --sf-choice-hover-bg: #f5f3ff;
  --sf-followup-border: #a78bfa;
  --sf-input-disabled-bg: #f9fafb;
}

CSS Parts

Style individual elements inside the shadow DOM using ::part():

PartDescription
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
survey-fields-stepWrapper for the Survey Fields step
completion-stepWrapper for the completion/thank-you screen
headingAll heading elements
poll-headingThe poll question heading
follow-up-headingThe follow-up question heading
survey-fields-headingThe Survey Fields heading
completion-headingThe completion screen heading
progressTop progress header wrapper
progress-indicatorCompatibility alias for the progress header
progress-labelTask or step count label
progress-textCompatibility alias for the progress label
progress-trackContinuous progress bar track
progress-barCompatibility alias for the progress bar track
progress-fillThe filled portion of the progress bar
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 Survey 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
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(survey-fields-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(survey-fields-heading) { }
sf-fastpoll::part(completion-heading) { }

/* Progress */
sf-fastpoll::part(progress) { }
sf-fastpoll::part(progress-indicator) { }
sf-fastpoll::part(progress-label) { }
sf-fastpoll::part(progress-text) { }
sf-fastpoll::part(progress-track) { }
sf-fastpoll::part(progress-bar) { }
sf-fastpoll::part(progress-fill) { }

/* 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) { }

/* Messages & States */
sf-fastpoll::part(message) { }
sf-fastpoll::part(error-message) { }
sf-fastpoll::part(loading-message) { }
sf-fastpoll::part(error-container) { }
sf-fastpoll::part(troubleshooting-link) { }
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

BrowserVersionNotes
Chrome88+Full support
Firefox85+Full support
Safari14+Full support
Edge88+Full support
IE11Not supportedRequires modern Web Component APIs, fetch, and AbortController