@sensefolks/pricepoint
v5.1.1
Published
Find the right price that your users are willing to pay
Maintainers
Readme
sf-pricepoint
Price sensitivity survey web component for Van Westendorp, Newton–Miller–Smith, and direct price testing.
Features
- 💰 Van Westendorp price sensitivity plus Newton–Miller–Smith purchase intent
- 🔄 Server-assigned VW + NMS or independent GG-derived direct price test
- 💱 Multi-currency support with auto-detection from respondent locale
- ♿ Accessibility-oriented keyboard, screen reader, and reduced motion support
- 🎨 Customizable via CSS Parts and Custom Properties
- 📱 Responsive design
- 🌐 Works with React + Next.js, Vue + Nuxt, Angular, Svelte, Astro, and vanilla HTML
Required respondent fields are indicated and keep submission disabled until valid without showing required-field errors. Email format feedback starts after any non-empty input and the respondent pauses typing for 1.5 seconds.
Research flow and deployment
Deploy compatible embed-request-api and embed-response-api services before loading version 5.1.0. Configuration loading is followed by GET /v1/embed/pricePoint/:surveyKey/study; a missing or invalid study assignment shows a retry action instead of silently changing the research method. Existing clients can continue using the legacy ladder endpoint during rollout. Legacy surveys with currencyMode: "auto" receive the explicit PRICEPOINT_FIXED_CURRENCY_REQUIRED response and retain VW-only submission; adaptive research requires a new survey with a fixed currency. No other study error enables fallback.
Discovery respondents answer four VW questions followed by two NMS questions at their own bargain and expensive prices. Later respondents are assigned by the server to either the same VW + NMS flow or a GG-only group. The GG-only group evaluates one randomly assigned price without earlier VW or NMS questions. This is a monadic first-price test derived from GG, not a sequential GG ladder or a respondent-level willingness-to-pay estimate. Candidate grids and batch/validation policy are controlled by the server.
VW inputs start blank and accept decimal amounts of at least 1, including amounts above 100. Legacy priceRange metadata does not constrain them. Non-finite values and amounts beyond JavaScript's safe numeric magnitude are rejected as technical validation; there is no outlier detection, flagging, or filtering. The four answers must be strictly increasing. Currency and billing terms come from the saved survey configuration.
NMS and GG reuse the same five-point purchase-intent scale. Responses represent stated intent rather than observed purchases. The component does not display the product description; keep the offering and purchase context visible on the host page consistently for both groups.
Pricing answers are sent only on final submission. Assignment and answers exist only in the component's memory, and reload begins a blank response with a new assignment. Failed final submissions retry using the same signed assignment; the response service deduplicates that assignment. No partial-answer save or restore is performed. Existing answer-free attempt telemetry is unchanged. Respondent fields and optional bot verification appear only on the final NMS or GG step.
Installation
NPM
npm install @sensefolks/pricepointYarn
yarn add @sensefolks/pricepointCDN (Script Tag)
<!-- Modern browsers (ES modules) -->
<script type="module" src="https://unpkg.com/@sensefolks/[email protected]/dist/sf-pricepoint/sf-pricepoint.esm.js"></script>Or via jsDelivr:
<script type="module" src="https://cdn.jsdelivr.net/npm/@sensefolks/[email protected]/dist/sf-pricepoint/sf-pricepoint.esm.js"></script>Quick Start
<sf-pricepoint survey-key="your-survey-uuid"></sf-pricepoint>React
import '@sensefolks/pricepoint';
function App() {
return <sf-pricepoint survey-key="your-survey-uuid"></sf-pricepoint>;
}Vue
<template>
<sf-pricepoint survey-key="your-survey-uuid"></sf-pricepoint>
</template>
<script>
import '@sensefolks/pricepoint';
export default { name: 'App' };
</script>Angular
// app.module.ts
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import '@sensefolks/pricepoint';
@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class AppModule {}<!-- component.html -->
<sf-pricepoint survey-key="your-survey-uuid"></sf-pricepoint>Framework Notes
- React + Next.js: use the React snippet above. In Next.js, render surveys inside a
'use client'component. - Vue + Nuxt: use the Vue snippet above. In Nuxt pages that SSR, wrap the survey with
<ClientOnly>. - Svelte: load the package inside
onMountand render the samesf-*tag. - Astro: keep the
sf-*tag in markup and load the package with a client-side module import. - Vanilla JS: use the HTML/CDN snippet or a module import in a
<script type="module">block.
For complete copy-paste examples, see: https://sensefolks.com/docs/tutorials/embedding-a-survey/
API Reference
Properties
| Property | Attribute | Type | Default | Description |
| ------------------- | -------------------- | -------- | -------------------------------- | -------------------------------------------------- |
| surveyKey | survey-key | string | undefined | Public key (UUID) of the survey to load (required) |
| completionMessage | completion-message | string | 'Thank you for your response!' | Fallback message shown after submission when the survey has no saved thank-you message |
| thankYouMessage | thank-you-message | string | undefined | Overrides the saved survey thank-you message |
| sessionData | — | object | {} | Every schema field is required when a schema exists; schema-less surveys accept safe flat string, finite number, and boolean values |
For schema-backed surveys, assign the complete sessionData object before setting surveyKey. Reassign a new object whenever Session Data values change; the component refetches after a meaningful reassignment.
Events
| Event | Detail Type | Description |
| --------------- | -------------------------- | ------------------------------------------------- |
| sfReady | SfReadyEventDetail | Survey config loaded and component ready |
| sfSubmit | SfSubmitEventDetail | Response successfully submitted |
| sfError | SfErrorEventDetail | Error occurred (load, submit, validation, config) |
| sfPhaseChange | SfPhaseChangeEventDetail | Phase transition (loading → vw → nms → complete, or loading → gg → complete) |
Event Payload Shapes
interface SfReadyEventDetail {
surveyKey: string;
productType: string;
phase: string;
}
interface SfSubmitEventDetail {
surveyKey: string;
phase: string;
}
interface SfErrorEventDetail {
surveyKey: string;
errorType: 'load' | 'submit' | 'validation' | 'config';
errorMessage: string;
}
interface SfPhaseChangeEventDetail {
phase: string; // 'loading' | 'vw' | 'nms' | 'gg' | 'complete' | 'error'
}Configuration Schema
The survey is configured server-side. The component fetches config via the surveyKey.
{
"productContext": {
"productName": "My SaaS Product",
"productCategory": "Software",
"industry": "Technology"
},
"targetMarkets": ["US", "GB"],
"productType": "subscription",
"billingInterval": "monthly",
"priceRange": { "min": 10, "max": 200 },
"currencyMode": "USD",
"costAnalysis": {
"annualCostPerCustomer": 50,
"annualFixedExpenses": 100000,
"expectedAnnualCustomers": 5000,
"desiredProfitMargin": 30
}
}The study response contains { protocol: 'adaptive-v1', assignment, assignmentToken }. The assignment records its ID, study version, phase, cohort, batch version, candidate grid, assigned price, and allocation probabilities. The final responseData.study carries the signed assignment unchanged. A VW + NMS response includes vanWestendorp and nms: { bargain: { price, purchaseIntent }, expensive: { price, purchaseIntent } }. A GG-only response includes one gaborGranger.responses entry and priceLadderVersion equal to the assignment's batch version, with no VW or NMS answers.
CSS Shadow Parts
The component uses shadow DOM and exposes the following CSS parts for styling:
| Part | Description |
| ----------------------- | ----------------------------------------------------------------- |
| survey-container | Outer wrapper for the entire survey |
| heading | Compatibility hook for primary body text |
| progress | Top progress header wrapper |
| progress-indicator | Compatibility alias for the progress header |
| progress-label | "Step X of Y" label |
| progress-text | Compatibility alias for the progress label |
| progress-track | Continuous progress bar track |
| progress-bar | Compatibility alias for the progress bar track |
| progress-fill | Filled portion of the progress bar |
| vw-container | Van Westendorp phase wrapper |
| question-container | Wrapper for each question |
| price-input | Blank decimal price input; text-input compatibility alias |
| text-input | Compatibility part on the open price input |
| price-preview | Currency display or minimum-price guidance |
| validation-message | Price input validation feedback |
| question-text | Question text paragraph |
| validation-errors | Validation error container |
| validation-error | Individual validation error message |
| button-container | Wrapper for action buttons |
| submit-button | The submit/next action button |
| error-container | Wrapper for error state display |
| error-message | Error message text |
| survey-field-error-message | Inline email feedback shown after a 1.5-second idle delay |
| troubleshooting-link | Troubleshooting link shown with integration errors |
| loading-message | Loading state text |
| completion-step | Completion/thank-you screen wrapper |
| completion-heading | Compatibility hook for the completion message |
| success-message | Green completion status message |
| hcaptcha-container | Wrapper for the hCaptcha widget (when enabled) |
| branding | SenseFolks branding wrapper |
| branding-link | SenseFolks branding link |
| branding-logo | SenseFolks branding logo |
| gg-container | Shared NMS/GG purchase-intent phase wrapper |
| price-display | Price value display span |
| radio-group | Wrapper for radio button group |
| radio-option | A single radio option row |
| radio-option-selected | Applied to the selected radio option (additive with radio-option) |
| radio-input | Survey Field radio input |
| radio-label | Radio option label text |
| survey-fields-container | Survey Fields section wrapper |
| form-container | Form container compatibility alias |
| survey-fields-heading | Survey Fields introductory text |
| form-field | Individual Survey Field wrapper |
| form-label | Survey Field label |
| required-indicator | Required field marker |
| survey-field-control | Survey Field input wrapper |
| form-input | Text/email/number Survey Field input |
| form-select | Dropdown Survey Field input |
| field-error | Survey Field validation error |
| checkbox-group | Checkbox Survey Field group |
| checkbox-option | Checkbox Survey Field option wrapper |
| checkbox-input | Checkbox Survey Field input |
| checkbox-label | Checkbox Survey Field label |
| announcements | ARIA live region for screen reader announcements |
CSS Custom Properties
| 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-error-text | #991b1b | Error message text color |
| --sf-error-bg | #fef2f2 | Error state background |
| --sf-error-border | #fecaca | Error state border color |
| --sf-success-text | #166534 | Success message text color |
| --sf-success-bg | #f0fdf4 | Success state background |
| --sf-success-border | #bbf7d0 | Success state border color |
| --sf-card-bg | #ffffff | Card/container background |
| --sf-card-border | #d1d5db | Card/container border color |
| --sf-button-text | #ffffff | Button text color |
| --sf-card-radius | 8px | Card/container border radius |
| --sf-button-radius | 6px | Button border radius |
| --sf-transition | 150ms ease | Default transition timing |
| --sf-font-family | system-ui, sans-serif | Font family (backward-compat) |
| --sf-spacing-scale | 1 | Spacing multiplier — 1 = default |
| --sf-progress-bg | #e5e7eb | Progress track color |
| --sf-progress-fill | primary | Progress fill color |
Legacy aliases
--sf-primary-color,--sf-text-color,--sf-background-color, and--sf-border-radiusare still honoured via CSS fallback chains for backward compatibility.
Styling example
/* Override primary color and card radius */
sf-pricepoint {
--sf-primary: #7c3aed;
--sf-primary-hover: #6d28d9;
--sf-card-radius: 12px;
--sf-button-radius: 8px;
}
/* Style the submit button */
sf-pricepoint::part(submit-button) {
font-weight: 600;
letter-spacing: 0.025em;
text-transform: uppercase;
}
/* Highlight the selected radio option */
sf-pricepoint::part(radio-option-selected) {
box-shadow: 0 0 0 2px var(--sf-primary);
}
/* Style the price display in the Gabor-Granger phase */
sf-pricepoint::part(price-display) {
font-size: 1.25rem;
font-weight: 700;
}Privacy And Network Requirements
sf-pricepoint loads survey configuration from the SenseFolks embed request endpoint and submits responses to the SenseFolks embed response endpoint. The survey-key value is validated as a UUID before requests are made and is URL encoded in request paths.
Submitted payloads can include the respondent's answers, configured survey fields, prepared session data values supplied by the host page, completion timing, and metadata including user-agent string, platform, language, cookie-enabled status, online status, screen resolution, color depth, timezone, and timestamp. This component does not use cookies, localStorage, sessionStorage, or IndexedDB. In-progress answers live only in the current component instance; reloading the page or creating a new instance always starts a blank response.
If hCaptcha is enabled by the survey config, allow hCaptcha in your CSP: script-src https://js.hcaptcha.com, frame-src https://hcaptcha.com https://*.hcaptcha.com, and a connect-src that includes your SenseFolks API origin plus the hCaptcha endpoints.
Accessibility
- Full keyboard navigation (Tab, Enter, Space, Arrow keys)
- Associated labels, decimal keyboard hints, and
aria-invalid/aria-describedbyfeedback on price inputs - ARIA
role="radiogroup"on NMS and GG purchase intent scales aria-live="polite"region for screen reader announcements on phase/question transitions- Visible focus indicators on all interactive elements
- Respects
prefers-reduced-motion: reduce - High contrast mode support via
@media (prefers-contrast: high)
Browser Support
- Chrome 88+
- Firefox 85+
- Safari 14+
- Edge 88+
Internet Explorer 11 is not supported. The host browser must provide modern
Web Component APIs, fetch, and AbortController.
Agent Integration
This section is designed for AI code generation agents integrating the sf-pricepoint component.
Machine-Readable Config Example
{
"component": "sf-pricepoint",
"package": "@sensefolks/pricepoint",
"attributes": [
{
"name": "survey-key",
"type": "string",
"required": true,
"description": "Public key UUID of the survey"
},
{
"name": "completion-message",
"type": "string",
"required": false,
"description": "Fallback message shown after submission when the survey has no saved thank-you message. Defaults to \"Thank you for your response!\""
}
],
"events": [
{ "name": "sfReady", "detail": "SfReadyEventDetail", "description": "Component loaded and ready" },
{ "name": "sfSubmit", "detail": "SfSubmitEventDetail", "description": "Response submitted" },
{ "name": "sfError", "detail": "SfErrorEventDetail", "description": "Error occurred" },
{ "name": "sfPhaseChange", "detail": "SfPhaseChangeEventDetail", "description": "Phase changed" }
],
"cssCustomProperties": [
"--sf-primary",
"--sf-primary-hover",
"--sf-text-primary",
"--sf-text-secondary",
"--sf-error-color",
"--sf-error-text",
"--sf-error-bg",
"--sf-error-border",
"--sf-success-text",
"--sf-success-bg",
"--sf-success-border",
"--sf-card-bg",
"--sf-card-border",
"--sf-button-text",
"--sf-card-radius",
"--sf-button-radius",
"--sf-transition",
"--sf-font-family",
"--sf-spacing-scale",
"--sf-progress-bg",
"--sf-progress-fill"
],
"cssParts": [
"survey-container",
"heading",
"progress",
"progress-indicator",
"progress-label",
"progress-text",
"progress-track",
"progress-bar",
"progress-fill",
"vw-container",
"question-container",
"question-text",
"price-input",
"text-input",
"price-preview",
"validation-message",
"validation-errors",
"validation-error",
"button-container",
"submit-button",
"error-container",
"error-message",
"survey-field-error-message",
"troubleshooting-link",
"loading-message",
"completion-step",
"completion-heading",
"success-message",
"hcaptcha-container",
"branding",
"branding-link",
"branding-logo",
"gg-container",
"price-display",
"radio-group",
"radio-option",
"radio-option-selected",
"radio-label",
"announcements"
]
}Emitted Events with Payload Shapes
| Event | Payload Fields | When Emitted |
| --------------- | ----------------------------------------------------------------- | --------------------------- |
| sfReady | surveyKey: string, productType: string, phase: string | After config fetch and init |
| sfSubmit | surveyKey: string, phase: string | After successful POST |
| sfError | surveyKey: string, errorType: string, errorMessage: string | On any error |
| sfPhaseChange | phase: string | On phase transition |
Integration Checklist
- Install the package:
npm install @sensefolks/pricepoint - Import the component:
import '@sensefolks/pricepoint'; - Add the element:
<sf-pricepoint survey-key="YOUR_KEY"></sf-pricepoint> - Listen for events:
el.addEventListener('sfReady', (e) => { ... }) - Apply custom styles via CSS custom properties or
::part()selectors - Ensure the survey is created and configured via the API before embedding
TypeScript Types
All types are exported from the package entry point:
import type {
PricepointSurveyConfig,
PriceLadderResponse,
CurrencyResolution,
VWAnswers,
SfReadyEventDetail,
SfSubmitEventDetail,
SfErrorEventDetail,
SfPhaseChangeEventDetail,
} from '@sensefolks/pricepoint';Changelog
See CHANGELOG.md.
License
MIT © SenseFolks
