All recipes
Recipe

NPS with faces

A standard NPS step is a row of eleven numbered buttons, 0 through 10. On mobile that row is hard to tap accurately and the choice between adjacent numbers is rarely meaningful. This recipe renders NPS as five faces instead. It loses strict 0–10 calibration in exchange for higher completion rates and a clearer sentiment signal. The picked face id lands on customStepResults['nps'] in the recorded session.

custom recipe

How was your experience?

The full source

Copy this file into your project. It's the same file the showcase preview above renders from.

components/NpsWithFaces.tsxtsx
/**
 * NPS step rendered with a 5-face emoji scale instead of the usual 0–10
 * numeric grid. Lower-friction signal: fewer choices to evaluate, no
 * ambiguity about which end is good.
 *
 * Wire it as a custom step type:
 *
 *   <CancelFlow
 *     steps={[
 *       ...,
 *       { type: 'nps', title: 'How was it?' },
 *     ]}
 *     customComponents={{ nps: NpsWithFaces }}
 *   />
 *
 * The picked face id lands on `customStepResults['nps']` in the recorded
 * session. Edit the FACES array to change labels or characters.
 */
import type { CustomStepProps } from '@churnkey/react/core'
import { useState } from 'react'

const FACES = [
  { id: 1, char: '😞', label: 'Hated it' },
  { id: 2, char: '🙁', label: 'Meh' },
  { id: 3, char: '😐', label: 'OK' },
  { id: 4, char: '🙂', label: 'Liked it' },
  { id: 5, char: '😍', label: 'Loved it' },
]

export function NpsWithFaces({ step, onNext }: CustomStepProps) {
  const [picked, setPicked] = useState<number | null>(null)
  const title = step.title ?? 'How was your experience?'

  return (
    <div className="ck-step">
      <h2 className="ck-step-title">{title}</h2>
      {step.description && <p className="ck-step-description">{step.description}</p>}

      <div style={{ display: 'flex', justifyContent: 'space-between', gap: 8, marginBottom: 24 }}>
        {FACES.map((f) => {
          const isSelected = f.id === picked
          return (
            <button
              key={f.id}
              type="button"
              onClick={() => setPicked(f.id)}
              aria-label={f.label}
              aria-pressed={isSelected}
              style={{
                appearance: 'none',
                textAlign: 'center',
                flex: 1,
                padding: '12px 0',
                border: `1.5px solid ${isSelected ? 'var(--ck-color-primary)' : 'var(--ck-color-border)'}`,
                background: isSelected ? 'var(--ck-color-primary-soft)' : 'var(--ck-color-surface)',
                borderRadius: 'var(--ck-radius-md)',
                fontSize: 28,
                cursor: 'pointer',
                transition: 'all var(--ck-motion-fast)',
                fontFamily: 'inherit',
              }}
            >
              {f.char}
            </button>
          )
        })}
      </div>

      <button
        type="button"
        className="ck-button ck-button-primary"
        onClick={() => onNext({ npsFace: picked })}
        disabled={picked === null}
      >
        Continue
      </button>
    </div>
  )
}

Wire it into a flow

Drop the file into your codebase and reference it from the appropriate prop on <CancelFlow>.

CancelButton.tsxtsx
import { CancelFlow } from '@churnkey/react'
import { NpsWithFaces } from './components/NpsWithFaces'

<CancelFlow
  steps={[
    { type: 'survey', reasons: [/* ... */] },
    { type: 'nps', title: 'How was your experience?' },  // custom step type
    { type: 'confirm' },
  ]}
  customComponents={{ nps: NpsWithFaces }}             // register the renderer
  handleCancel={async () => myBilling.cancel()}
/>