# Implementing custom surveys

#### Contents

- Rendering surveys programmatically
- Using displaySurvey (recommended)
- Popover surveys
- Pre-filling survey responses
- Inline surveys
- Using renderSurvey (deprecated)
- Fetching surveys manually
- Wait for feature flags before checking which surveys match
- Example: a dynamic feedback form
- Capture survey responses
- Response formats
- Capturing multiple responses
- Capture survey lifecycle events

> Custom surveys are currently supported only with the [JavaScript Web SDK](/content/docs/libraries/js/index.html). You aren't limited to the surveys PostHog renders for you, though. Because a survey is just a set of questions you fetch and responses you capture, you can [fetch your surveys](/content/docs/surveys/implementing-custom-surveys#fetching-surveys-manually/index.html) and [capture responses](/content/docs/surveys/implementing-custom-surveys#capture-survey-responses/index.html) yourself and show them with your own UI.

At its most basic level, a survey is a collection of response events. If you wanted to, you could capture events from anywhere, like in this example of a hardcoded Next.js feedback survey:

```javascript
'use client'
import { usePostHog } from '@posthog/react';

export default function Feedback() {
  const posthog = usePostHog();

const handleFeedbackSubmit = (e) => {
    e.preventDefault();
    const feedback = e.target.elements.feedback.value;
    posthog.capture("survey sent", {
      $survey_id: '018ed910-3b24-0000-e3fd-d51c59aa74b2',
      $survey_questions: [
        {
          id: "a3071551-d599-4eeb-9ffe-69e93dc647b6",
          question: "Do you like PostHog?",
        }
      ],
      $survey_response: "Yes!"
    })
  }

return (
    <div>
      <h1>Give us feedback</h1>
      <form onSubmit={handleFeedbackSubmit}>
        <textarea
          id="feedbackInput"
          name="feedback"
          placeholder="Enter your feedback here..."
          required
        ></textarea>
        <button type="submit">Submit Feedback</button>
      </form>
    </div>
  );
}
```

The benefit of using PostHog beyond this is that it handles:

1. **Survey content.** Customize question type, text, and more. Change it at runtime without needing to redeploy your app.

2. **Display conditions.** This means not showing the same survey multiple times, the wrong survey, or a survey that has collected enough responses. Leverage property filters, cohorts, feature flags, and more.

If you create a [popover survey](/content/docs/surveys/creating-surveys#presentation/index.html), updating and display conditions are handled automatically. When you create one in **API mode**, you need to add logic to fetch and display surveys yourself with the help of the [JavaScript Web SDK or snippet](/content/docs/libraries/js/index.html).

## Rendering surveys programmatically

Although we recommend using popover surveys and display conditions, if you want to show surveys programmatically without setting up all the extra logic needed for API surveys, you can render surveys programmatically with the `displaySurvey` (recommended) or `renderSurvey` (deprecated) method.

> **Important:** Surveys are not immediately available on page load. If you call `renderSurvey` before surveys have been initialized, the call may fail. To avoid this, use the `posthog.onSurveysLoaded(callback)` method. This ensures surveys are fully initialized before attempting to render them.

### Using displaySurvey (recommended)

The `displaySurvey` method is the recommended way to show surveys programmatically. It supports two display types:

1. **Popover surveys** \- Shows surveys as styled popovers (same as regular surveys)
2. **Inline surveys** \- Renders surveys within a specific HTML element

#### Popover surveys

To display a survey as a popover, simply call `displaySurvey` with the survey ID:

```javascript
import React from 'react';
import { DisplaySurveyType } from 'posthog-js';
import { usePostHog } from '@posthog/react';

const coolSurveyID = "01942e4c-d028-0000-6ab5-afb4ed961263"

function App() {
  const posthog = usePostHog();

useEffect(() => {
    // Will be called only once on first load, respecting conditions defined in dashboard
    posthog.displaySurvey(coolSurveyID, {
      displayType: DisplaySurveyType.Popover,
      ignoreConditions: false,
      ignoreDelay: false
    });
  }, []);

const handleForceShowSurvey = () => {
    // Force display ignoring conditions and delays
    posthog.displaySurvey(coolSurveyID, {
      displayType: DisplaySurveyType.Popover,
      ignoreConditions: true,
      ignoreDelay: true
    });
  };

return (
    <div className="App">
      <h1>Survey Tutorial with PostHog</h1>
      <div className="survey-controls">
        <button onClick={handleForceShowSurvey}>
          Force Show Survey
        </button>
      </div>
    </div>
  );
}

export default App;
```

> **Note:** With `ignoreConditions: false`, this checks the survey's display conditions, which can depend on feature flags. See [waiting for feature flags](/content/docs/surveys/implementing-custom-surveys#wait-for-feature-flags-before-checking-which-surveys-match/index.html) if your survey is targeted with a flag.

#### Pre-filling survey responses

You can pre-fill survey answers programmatically by passing `initialResponses` to `displaySurvey`. This works with single choice, multiple choice, and rating questions, and is only supported for **popover** surveys.

```javascript
import React from 'react';
import { DisplaySurveyType } from 'posthog-js';
import { usePostHog } from '@posthog/react';

const coolSurveyID = "01942e4c-d028-0000-6ab5-afb4ed961263"

function App() {
  const posthog = usePostHog();

const handleShowPrefilled = () => {
    posthog.displaySurvey(coolSurveyID, {
      displayType: DisplaySurveyType.Popover,
      initialResponses: {
        0: 2,          // Single choice: select the 3rd choice (0-based index)
        1: 9,          // Rating: pre-fill with a rating of 9
        2: [0, 2]      // Multiple choice: select the 1st and 3rd choices (0-based indices)
      }
    });
  };

return (
    <div className="App">
      <h1>Survey Tutorial with PostHog</h1>
      <div className="survey-controls">
        <button onClick={handleShowPrefilled}>
          Show Pre-filled Survey
        </button>
      </div>
    </div>
  );
}

export default App;
```

**`initialResponses` format:**

- Keys are question indices (0-based: `0` for first question, `1` for second, etc.)
- Values use the same format as URL prefill parameters:
  - **Single choice**: Choice index as a number (e.g., `2` for the 3rd choice)
  - **Multiple choice**: Array of choice indices (e.g., `[0, 2]` for the 1st and 3rd choices)
  - **Rating**: Rating value as a number (e.g., `9` for a 0-10 scale)

**Important notes:**

- Only supported for popover surveys — inline surveys will log a warning and ignore the prefill
- Choice and question indices are 0-based
- Invalid indices are silently ignored
- Open text and link questions do not support prefill
- Pre-filled questions with "Automatically submit on selection" enabled will be skipped, advancing the user to the first non-prefilled question

#### Inline surveys

To display a survey inline within a specific element, specify the `displayType` and `selector`:

```javascript
import React from 'react';
import { DisplaySurveyType } from 'posthog-js';
import { usePostHog } from '@posthog/react';

const coolSurveyID = "01942e4c-d028-0000-6ab5-afb4ed961263"

function App() {
  const posthog = usePostHog();

useEffect(() => {
    posthog.displaySurvey(coolSurveyID, {
      displayType: DisplaySurveyType.Inline,
      ignoreConditions: false,
      ignoreDelay: false,
      selector: '#survey-container'
    });
  }, []);

const handleForceShowInlineSurvey = () => {
    // Will ignore both conditions and delays
    posthog.displaySurvey(coolSurveyID, {
      displayType: DisplaySurveyType.Inline,
      ignoreConditions: true,
      ignoreDelay: true,
      selector: '#survey-container'
    });
  };

return (
    <div className="App">
      <h1>Survey Tutorial with PostHog</h1>
      <div className="survey-controls">
        <button onClick={handleForceShowInlineSurvey}>
          Force Show Inline Survey
        </button>
      </div>
      <div id="survey-container">
        <p>Survey will render here</p>
      </div>
    </div>
  );
}

export default App;
```

### Using renderSurvey (deprecated)

> **Note:** The `renderSurvey` method is deprecated in favor of `displaySurvey`. While `renderSurvey` will continue to work for backwards compatibility, we recommend using `displaySurvey` for all new implementations as it supports both styled and unstyled surveys with more flexible options.

The legacy `renderSurvey` method renders an **unstyled** survey in a specific HTML element, ignoring any display conditions:

```javascript
import './App.css';
import React from 'react';
import { usePostHog } from '@posthog/react';

function App() {
  const posthog = usePostHog();

const coolSurveyID = "01942e4c-d028-0000-6ab5-afb4ed961263"

const handleRenderSurvey = () => {
    posthog.renderSurvey(coolSurveyID, '#survey-container');
  };

return (
    <div className="App">
      <h1>Survey Tutorial with PostHog</h1>
      <div className="survey-controls">
        <button onClick={handleRenderSurvey}>
          Render Survey
        </button>
      </div>
      <div id="survey-container">
        <p>Survey will render here</p>
      </div>
    </div>
  );
}

export default App;
```

## Fetching surveys manually

For more control over your surveys, you can use API surveys. Fetching a survey and capturing its responses yourself is all you need to show it with your own UI, without PostHog's built-in popover. This approach is sometimes called a headless survey. There are two options for fetching surveys from PostHog:

1. To get all surveys, call `getSurveys(callback, forceReload)`. This means you still need to handle display conditions yourself.

2. To get surveys enabled for the current user, call `getActiveMatchingSurveys(callback, forceReload)`. This returns the active surveys the user matches the display conditions for, as long as the user (determined by their distinct ID) has not already dismissed or responded to the survey.

Surveys are requested on first load and then cached by default by the JavaScript SDK. If you want to force an API call to get an updated list of surveys, pass `true` to the `forceReload` parameter. You only need to do this if you want changed surveys to appear mid-session for users.

Both methods return a callback with an array of surveys in this format:

```json
// Example surveys response:
[{\
  "id": "your_survey_id",\
  "name": "Your survey name",\
  "description": "Metadata describing your survey",\
  "type": "api", // "api" or "popover"\
  "linked_flag_key": null, // Linked feature flag key, if any.\
  "targeting_flag_key": "your_survey_targeting_flag_key",\
  "questions": [\
    {\
      "type": "single_choice",\
      "choices": [\
        "Yes",\
        "No"\
      ],\
      "question": "Are you enjoying PostHog?",\
      "id": "7ce5b831-dc71-4648-b6b0-b583d48a0c11" // Each question has a UUID\
    }\
  ],\
  "conditions": null,\
  "appearance": {},\
  "start_date": "2023-09-19T13:10:49.505000Z",\
  "end_date": null\
}]
```

### Wait for feature flags before checking which surveys match

Display conditions often depend on feature flags. Targeting a survey at a set of users creates a flag behind the scenes, and linking a survey to a flag uses one directly. Flags are fetched after the page loads, so they are not available immediately.

`getActiveMatchingSurveys` reports what the SDK knows at the moment you call it, and it calls your callback once. If you call it before flag values arrive, you can get an empty list for a user who does match, and you won't be called again when the flags land.

Call it inside `posthog.onFeatureFlags()` to avoid this. That callback runs as soon as flag values are available, and runs right away if they already are. It also returns a function you can call to unsubscribe.

```javascript
posthog.onFeatureFlags(() => {
  posthog.getActiveMatchingSurveys((surveys) => {
    // Flag values are available here, so display conditions are evaluated against them
  });
});
```

This applies to `displaySurvey` with `ignoreConditions: false` too, since it checks the same display conditions. If none of your surveys use flag targeting or a linked flag, you can call `getActiveMatchingSurveys` directly.

### Example: a dynamic feedback form

As an example, we can use `getActiveMatchingSurveys()` to make our Next.js feedback survey more dynamic:

```javascript
'use client'
import { usePostHog } from '@posthog/react';
import { useEffect, useState } from 'react';

export default function Feedback() {
  const [survey, setSurvey] = useState({})
  const posthog = usePostHog()

useEffect(() => {
    // Wait for feature flags so surveys targeted with a flag are evaluated against real values
    return posthog.onFeatureFlags(() => {
      posthog.getActiveMatchingSurveys((surveys) => {
        if (surveys.length > 0) {
          const survey = surveys[0];
          setSurvey(survey)
        }
      });
    });
  }, [posthog]);

const handleFeedbackSubmit = (e) => {
    e.preventDefault();
    const feedback = e.target.elements.feedback.value;
    const responseKey = `$survey_response_${survey.questions[0].id}`; // $survey_response_7ce5b831-dc71-4648-b6b0-b583d48a0c11
    posthog.capture("survey sent", {
      $survey_id: survey.id,
      [responseKey]: feedback
    })
  }

return (
    <div>
      {survey && (
        <>
          <h1>{survey.questions[0].question}</h1>
          <form onSubmit={handleFeedbackSubmit}>
            <textarea
              id="feedbackInput"
              name="feedback"
              placeholder={survey.appearance.placeholder}
              required
            ></textarea>
            <button type="submit">Submit Feedback</button>
          </form>
        </>
      )}
    </div>
  );
}
```

> **Tip:** To keep track of surveys shown, dismissed, or responded to, store values in a cookie or local storage like this:
> ```javascript
> //... other code
> posthog.capture("survey sent", {
>   $survey_id: survey.id,
>   $survey_response_7ce5b831-dc71-4648-b6b0-b583d48a0c11: feedback
> })
> localStorage.setItem(`hasInteractedWithSurvey_${survey.id}`, 'true');
> ```

## Capture survey responses

The main event you must capture to track survey results is `survey sent`. PostHog supports two formats for survey responses, but we strongly recommend using ID-based responses as they are more reliable and resistant to changes in question ordering:

```javascript
/**
 * Example survey response:
 *
 * [{\
 *   "id": "your_survey_id",\
 *   ...otherFields,\
 *   "questions": [\
 *     {\
 *       ...otherQuestionFields,\
 *       "question": "Are you enjoying PostHog?",\
 *       "id": "7ce5b831-dc71-4648-b6b0-b583d48a0c11" // Each question has a UUID\
 *     },\
 *     {\
 *       ...otherQuestionFields,\
 *       "question": "What is your favorite color?",\
 *       "id": "cbeee176-54e0-4757-befd-f4e8fb33b9a9"\
 *     }\
 *   ],\
 * }]*/

const responses = survey.questions.map((question) => {
  const responseKey = `$survey_response_${question.id}`;
  return {
    [responseKey]: questionResponse // this is the response you're tracking in your survey implementation
  }
})

posthog.capture("survey sent", {
    $survey_id: survey.id, // required
    ...responses
})
```

For backward compatibility, PostHog also supports index-based responses, but these are more fragile as they depend on question order:

```javascript
posthog.capture("survey sent", {
    $survey_id: survey.id,
    $survey_response: questionResponseForFirstQuestion, // Index-based (legacy, position 0)
    $survey_response_1: questionResponseForSecondQuestion // Index-based (legacy, position 1)
})
```

### Response formats

Survey responses expect text, so you should convert numbers to text e.g. `8` should be converted to `"8"`.

For multiple choice surveys, the response must be an array of values with the selected choices e.g., `$survey_response_multiple_choice_1: ["response_1", "response_2"]`.

### Capturing multiple responses

For surveys with multiple questions, use ID-based properties to ensure your responses remain valid even if questions are reordered:

```javascript
/**
 * Example survey response:
 *
 * [{\
 *   "id": "your_survey_id",\
 *   ...otherFields,\
 *   "questions": [\
 *     {\
 *       ...otherQuestionFields,\
 *       "question": "What can we do to improve our product?",\
 *       "type": "open_text",\
 *       "id": "cbeee176-54e0-4757-befd-f4e8fb33b9a9"\
 *     },\
 *     {\
 *       ...otherQuestionFields,\
 *       "question": "How can we improve our Surveys product?",\
 *       "type": "rating",\
 *       scale: 10,\
 *       "id": "7ce5b831-dc71-4648-b6b0-b583d48a0c11" // Each question has a UUID\
 *     },\
 *     {\
 *       ...otherQuestionFields,\
 *       "question": "What should we build next?",\
 *       "type": "multiple_choice",\
 *       "choices": ["Better analysis tools", "Better documentation and more examples", "More templates"],\
 *       "id": "cbeee176-54e0-4757-befd-f4e8fb33b9a9"\
 *     }\
 *   ],\
 * }]*/

const responses = survey.questions.map((question) => {
  const responseKey = `$survey_response_${question.id}`;

if (question.type === 'multiple_choice') {
    return {
      [responseKey]: ["Better analysis tools", "More templates"] // this is the response you're tracking in your survey implementation
    }
  }

return {
    [responseKey]: questionResponse // this is the response you're tracking in your survey implementation
  }
})

posthog.capture("survey sent", {
    $survey_id: survey.id, // required
    ...responses
})
```

While index-based responses are still supported, we recommend using ID-based responses for all new implementations:

```javascript
// Legacy format (not recommended for new implementations)
posthog.capture("survey sent", {
    $survey_id: survey.id,
    $survey_response: "PostHog is a great tool",
    $survey_response_1: 10,
    $survey_response_2: ["More templates"]
})
```

> **Note:** While PostHog's survey results page supports both formats, custom dashboards and insights might need updates if you switch from index-based to ID-based responses. We recommend planning this transition carefully if you have existing custom analytics based on survey responses.

## Capture survey lifecycle events

There are two other events you should capture to track the full lifecycle of a survey. They are `survey shown` and `survey dismissed`:

```javascript
// 1. When a user is shown a survey
posthog.capture("survey shown", {
    $survey_id: survey.id // required
})

// 2. When a user has dismissed a survey
posthog.capture("survey dismissed", {
    $survey_id: survey.id // required
})
```

Capturing all three events ensures you have a full implementation matching popup surveys and that your analysis is accurate in PostHog.

### Still have questions?

Ask PostHog AI

### Was this page useful?

HelpfulCould be better
