Skip to content

Building Surveys in jsPsych

Choosing which jsPsych survey plugin to use

jsPsych has several plugins that allow you to present survey questions during your experiment. The one you choose will depend on what exactly you'd like to do, and your preferences for convenience/parameterization versus flexibility.

  • Survey-* plugins: survey-likert, survey-multi-choice, survey-multi-select, survey-text
    • Only one question type and one page per trial
    • Parameterization makes it easy to define questions
    • Work well with timeline variables
    • Ideal for adding survey-style questions into repetitive trial procedures
    • Limited functionality and customization options
  • survey-html-form plugin
    • Can mix different question types on the same page
    • No parameters for defining questions - you write the form HTML
    • Maximally flexible, so ideal if you need a lot of control over the survey content and style
  • survey plugin
    • Not well-suited for use with timeline variables
    • Large set of built-in question types and parameterized customization options
    • Can mix different question types on the same page
    • Can present mutliple pages that participants can navigate back and forth through, without losing responses
    • Parameters for defining questions, but more config/code than the survey-* plugins
    • Built-in convenience parameters for many survey question types and features (e.g. response validation, conditional question display, 'other'/'none'/'select all' options)

The survey plugin differs from most other jsPsych plugins in that is a simple wrapper for an external JavaScript library called SurveyJS. This allows jsPsych users to take advantage of all of the SurveyJS features, documentation, and example code, but it also means that the survey plugin does not follow the same familiar conventions as most other jsPsych plugins. Users will need to familiarize themselves somewhat with the SurveyJS library in order to use the plugin. The remaining documentation on this page provides some guidance for getting started with SurveyJS and the jsPsych survey plugin.

Getting started with SurveyJS

The SurveyJS form library is a large and powerful survey-building framework with its own helpful documentation, examples, and demos. Here we have tried to orient jsPsych users to the basic steps for constructing surveys and highlight the features that jsPsych users may find most useful. However, it is not possible for us to reproduce the SurveyJS documentation here, so we encourage you to take advantage of their comprehensive documentation and demo/code examples.

SurveyJS allows you to build surveys using a JavaScript/JSON object, a JavaScript function, or a combination of both. You can read more about these options in the SurveyJS documentation:

The jsPsych survey plugin provides the survey_json and survey_function parameters to allow you to construct a SurveyJS survey using these JSON and JavaScript methods. The next two sections on this page explain more about each method: Creating a survey with JSON and Using JavaScript to create or modify the survey.

Here are some other places to start learning about SurveyJS:

SurveyJS has some more specific features that some researchers might find useful, including:

You can find realistic examples on the SurveyJS examples/demos page. And to view all of the survey-level options, see the Survey API documentation.

Creating a survey with JSON

SurveyJS allows you to define the survey contents using an object with parameters names and values. At a minimum, the survey JSON object should contain a property called 'elements'. The value of 'elements' is an array that contains at least one element/question to be shown on the page.

// Survey with a single text entry question.
const survey_json = {
  elements: [{
    name: "example",
    title: "Enter some text in the box below!",
    type: "text"
  }]
};

Each element is an object with a 'type', which is the element/question type (see the survey plugin's Questions/Elements section for a list of type options). The element objects should also contain any other parameters and configuration options for that question, such as the question name (used to identify the question in the data), title (prompt shown to the participant), whether or not a response is required, and other parameters that might be relevant to that particular question type. The Questions/Elements section in the survey plugin documentation contains links to the SurveyJS documentation for each question type, where you can find more information about the required and optional parameters.

Once you've created the survey JSON object, as we've done above, it can be used as the survey_json parameter in a jsPsych survey trial:

const survey_trial = {
  type: jsPsychSurvey,
  survey_json: survey_json
};

timeline.push(survey_trial);

That's it! The code above will create a valid survey.

JSON vs JavaScript objects

JSON (JavaScript Object Notation) is a text format for organizing data. It is very similar to a JavaScript object, but not exactly the same. The survey_json parameter takes a JSON-compatible JavaScript object, rather than a JSON string. We use the 'JSON' term for this parameter to make it clear that this parameter should not contain functions, and for consistency with SurveyJS documentation. To read more about JSON vs JavaScript objects, see e.g. here and here.

Multiple pages

You can specify 'elements' as a top-level property in the survey JSON, and those elements will be shown on a single page. If you'd like the survey to have more than one page, then you can add a 'pages' property to the survey JSON object. The value of 'pages' should be an array of objects, where each object defines a single page. Each page object should contain its own 'elements' array.

The example below defines a survey with two pages. Each page has a set of elements/questions, as well as some optional parameters (page name and title).

const survey_json = {
  pages: [
  {
    name: "page_1",
    title: "Your Name",
    elements: [{
      type: "text",
      name: "first_name",
      title: "Enter your first name:"
    }, {
      type: "text",
      name: "last_name",
      title: "Enter your last name:"
    }
  }, {
    name: "page_2",
    title: "Personal Information",
    elements: [{
      type: "text",
      name: "location",
      title: "Where do you live?"
    }, {
      type: "text",
      name: "occupation",
      title: "What is your occupation?"
    }, {
      type: "text",
      name: "age",
      title: "How old are you?",
      inputType: "number",
      min: 0,
      max: 120
    }
  }]
};

Survey-level options

Along with either the 'elements' or 'pages' property, you can add optional survey-level properties to the top-level of your survey JSON object. The survey-level properties might include things like: a title (shown at the top of each page), whether to use automatic question numbering, labels for the page navigation buttons, and text to use for marking required questions. These and other survey-level parameters are not required - you only need to set these values if you want to change them from the defaults.

const survey_json = {
  title: "Survey title",
  showQuestionNumbers: "off",
  completeText: "Done",
  pageNextText: "Next",
  pagePrevText: "Back",
  requiredText: "[REQUIRED]",
  pages: [{
    elements: {
      // ... page 1 questions
    }
  }, {
    elements: {
      // ... page 2 questions
    }
  }]
};

Some of the survey-level options can also be set a the page level. See the Page API documentation for more information.

For more survey JSON examples, see the SurveyJS JSON documentation, the Examples section on the survey plugin documentation page, and the examples folder in the survey plugin package.

Using JavaScript to create or modify the survey

SurveyJS allows you to create or modify your survey using JavaScript. The JavaScript approach can do any of the configuration that can be done in JSON, plus it allows you to make your survey more dynamic. For instance, you could use the survey function parameter to change the contents of the survey based on the participant's earlier responses. The survey function parameter also allows you to define any other functions that should run during the survey. For instance, you might want to run custom code in response to a page change or response input event.

In the jsPsych survey plugin, the survey_function parameter receives a 'survey' argument, which is a SurveyJS survey model that you can manipulate. If you do not include a value for the survey_json parameter, then the survey_function will receive an empty survey. In this case, your survey_function must add at least one page with at least one element/question to produce a valid survey.

Here's the JavaScript function that would create the same survey that's defined in the first JSON example above:

const survey_function = (survey) => {
  // add page
  const page = survey.addNewPage("page1");
  // add question
  const text_question = page.addNewQuestion("text", "example");
  text_question.title = "Enter some text in the box below!";
};

const survey_trial = {
  type: jsPsychSurvey,
  survey_function: survey_function
};

timeline.push(survey_trial);

Combining JSON and function parameters

If you specify survey JSON using the survey_json parameter, then the survey_function will receive a survey object that was created using your JSON. This means that, in your survey function, you can access all of the survey elements that you have defined in the JSON.

Here's a slightly more realistic case for when you might want to use the survey_function parameter. In this example, we want to ask the participant to make a color choice at the start of the experiment, and then reference their choice in a later survey trial question. We can't do this with the JSON configuration because we cannot know the participant's color choice in advance - it only becomes available during the experiment.

However, we can use the survey_function to dynamically access the participant's color response from the jsPsych data and use that value in the survey question title. We'll use the survey_function just for this one dynamic part of the survey, and define everything else in JSON.

// Create an array of color choices
const color_choices = ['red', 'green', 'blue', 'yellow', 'pink', 'orange', 'purple'];

// Create an html-button-response trial where the participant can choose a color
const select_color_trial = {
  type: jsPsychHtmlButtonResponse,
  stimulus: '<p>Which of these is your favorite color?</p>',
  choices: color_choices,
  button_html: '<button class="jspsych-btn" style="color:%choice%";">%choice%</button>',
  data: {trial_id: 'color_trial'}
};

// Create the survey JSON 
const color_survey_json = {
  elements: [{
    type: "boolean",
    renderAs: "radio",
    name: "color_confirmation",
    title: "" // This value will be set in the survey function
  }]
};

// Create a survey function to access the participant's response 
// from an earlier trial and modify the survey accordingly
const color_survey_function = (survey) => {
  // Get the earlier color selection response (button index) from the jsPsych data
  const color_choice_index = jsPsych.data.get().filter({trial_id: 'color_trial'}).values()[0].response;
  const color_choice = color_choices[color_choice_index];
  // Get the question that we want to modify
  const color_confirmation_question = survey.getQuestionByName('color_confirmation');
  // Change the question title to include the name of the color that was selected
  color_confirmation_question.title = `
    Earlier you chose ${color_choice.toUpperCase()}. Do you still like that color?
  `;
}

// Create the jsPsych survey trial using both the survey JSON and survey function
const color_survey_trial = {
  type: jsPsychSurvey,
  survey_json: color_survey_json,
  survey_function: color_survey_function
};

jsPsych.run([select_color_trial, color_survey_trial]);

For more information about creating/modifying surveys with JavaScript, see the SurveyJS documentation. The SurveyJS API reference contains all of the properties (parameters), methods, and events you can use when working with the survey, page, and question objects.

Deciding between JSON and function parameters

You can create survey trials entirely with JSON, entirely with a JavaScript function, or using a combination of both. Sometimes this is just a matter of preference. But you must use the JavaScript survey_function method when you want to:

  • Dynamically modify the survey based on a participant's response from an earlier trial, or any other information you don't have access to before the survey trial begins.
  • Use custom functions as part of your survey's configuration. For instance, you might want to write a function that is triggered by a particular survey event, such as when response values change or when the survey is completed. You cannot put JavaScript functions into the survey_json object, so you will need to add them using the survey_function parameter.

Custom response validation

There is a special case where you don't need to use the survey_function parameter for running a custom function, which is for adding custom response validation. The survey plugin includes a convenience parameter called validation_function, which allows you to add some custom JavaScript code to validate responses. Of course, you can also use the survey_function parameter for this, in which case you would set your custom function to run in response to the survey's onValidateQuestion event.

Creating dynamic surveys with JSON

Although you cannot include JavaScript functions as values in your survey_json, SurveyJS has implemented some convenience options for setting up certain kinds of dynamic survey behavior from within the JSON configuration. For instance, you can define a condition expression from within the JSON that will dynamically show/hide a question or choice/column/row (see Conditional Visibility and Expressions). As another example, you can use a placeholder value inside a text string to insert the response from a particular question into that string (see Dynamic Texts: Question Values).

In general, you can access information in the survey JSON that exists within that same survey trial and use it to produce dynamic behavior (e.g. putting placeholder values in text strings, automatically populating choice values, etc.). But if you need to access information that becomes available during the experiment but outside of that particular survey trial, you will need to use the survey_function parameter.

Defining survey trials/questions programmatically

Sometimes it's useful to be able to create your survey content programmatically. For instance, let's say you want to present a page with questions that all use the exact same format but with different prompts. You could define them one-by-one in a survey_json object, but doing it this way might produce a very large JSON object with lots of repeated configuration across all questions.

Instead, it's often preferrable to separate the information that changes across questions from the things that stay the same. This can make it easier to make changes and prevent errors, since the things that are common across questions only need to be defined once. Similarly, if you are repeating a trial procedure lots of times, then you might want to define a single survey trial that repeats with slightly different parameters.

The following section presents some different options for programmatically defining multiple questions in a survey trial, or multiple survey trials, based on an array of values that should change across questions or trials.

The example below shows how to use the survey_function to loop over a set of question-level variables (titles/prompts and names), and dynamically add each question to a single survey page. You could use this same approach to add questions across multiple pages within the same survey trial.

const survey_function = (survey) => {

  // this array stores any information that changes across questions
  const questions = [
    {title: "Question 1", name: "q1"},
    {title: "Question 2", name: "q2"},
    // ... more question-level variables ...
    {title: "Question N", name: "qN"}
  ];

  // create a single page
  const page = survey.addNewPage("questions");

  for (let i=0; i<questions.length; i++) {
    // for each object in the questions array,
    // create a new question and add it to the same page
    let q = page.addNewQuestion("text", questions[i].name, i);
    q.title = questions[i].title;
    q.inputType = "range";
    q.min = 0;
    q.max = 100;
    q.step = 10;
    q.defaultValue = 50;
    q.isRequired = true;
  }

}

Use the 'matrix' question type for repeated response options

The example above was used to illustrate how you can loop over information to programmatically construct a series of questions that are shown on the same page. But in cases where you have different question prompts that all use the same multiple choice options, you might prefer to use the the SurveyJS "matrix" question type. This question type creates a table where each row is a question and each column is a response option. The table format is often used for Likert scales, satisfaction surveys, etc. You can even create a table that repeats a set of questions with different response types using the SurveyJS multi-select matrix ("matrixdropdown") question type.

Rather than repeating a question format within the same trial, perhaps you want to use trial-level variables to generate separate survey trials, for instance in order to incorporate them into a larger repeating trial procedure. jsPsych's timeline variables feature was designed to address this use case, but the use of timeline variables looks a little different with the survey plugin. This is because the various individual parameters that you might want to change across survey trials don't have their own plugin parameters - instead everything is nested within the survey_json parameter. Below are some examples showing how you can programmatically generate survey trials using a set of trial-level variables.

  1. Use the conventional timeline variables approach with the survey_json parameter. This approach is probably not ideal, because the timeline variables array has to contain the entire survey_json object for each trial. This kind of defeats the purpose of timeline variables, because you are still defining all of the survey JSON separately for each question/trial. In any case, here's what it looks like:

    const word_trials = {
      timeline: [
        {
          type: jsPsychHtmlKeyboardResponse,
          stimulus: '+',
          choices: "NO_KEYS",
          trial_duration: 500
        },
        {
          type: jsPsychHtmlKeyboardResponse,
          stimulus: jsPsych.timelineVariable('word'),
          choices: "NO_KEYS",
          trial_duration: 1000
        },
        {
          type: jsPsychSurvey,
          survey_json: jsPsych.timelineVariable('survey_json'),
          data: { word: jsPsych.timelineVariable('word') }
        }
      ],
      timeline_variables: [
        {
          word: 'cheese',
          survey_json: { elements: [ {type: "text", title: "Enter a word related to CHEESE:", autocomplete: "off" } ], showQuestionNumbers: false, completeText: "Next", focusFirstQuestionAutomatic: true } 
        },
        {
          word: 'ring',
          survey_json: { elements: [ {type: "text", title: "Enter a word related to RING:", autocomplete: "off" } ], showQuestionNumbers: false, completeText: "Next", focusFirstQuestionAutomatic: true } 
        },
        {
          word: 'bat',
          survey_json: { elements: [ {type: "text", title: "Enter a word related to BAT:", autocomplete: "off" } ], showQuestionNumbers: false, completeText: "Next", focusFirstQuestionAutomatic: true }
        },
        {
          word: 'cow',
          survey_json: { elements: [ {type: "text", title: "Enter a word related to COW:", autocomplete: "off" } ], showQuestionNumbers: false, completeText: "Next", focusFirstQuestionAutomatic: true }
        }
      ]
    };
    

    Consider using a survey-* plugin for presenting a single question type

    The example above was created just to demonstrate how to combine the survey plugin and timeline variables. But if this were a real experiment, since each survey trial contains just one question, we'd be better off using one of the other survey-* plugins because the parameterization of those plugins works well with timeline variables. Of course, you may have other reasons for wanting to use the survey plugin in this type of trial procedure, for instance to take advantage of some its convenience features (e.g. different question types on the same page, response validation).

  2. Use jsPsych's functions-as-parameters approach for the survey_json parameter. With this approach, instead of defining a static JSON object for the value of survey_json, you would write a function that returns the survey JSON object for that specific trial.

    This approach allows you to define the survey content using a combination of variable and static values. Also, jsPsych will run this function right before the trial begins, which means that you can change the survey content dynamically based on information that only becomes available during the experiment. Here's an example using timeline variables, though you can use the same approach to dynamically create the survey content without using timeline variables.

    const word_trials = {
      timeline: [
        {
          type: jsPsychHtmlKeyboardResponse,
          stimulus: '+',
          choices: "NO_KEYS",
          trial_duration: 500
        },
        {
          type: jsPsychHtmlKeyboardResponse,
          stimulus: jsPsych.timelineVariable('word'),
          choices: "NO_KEYS",
          trial_duration: 1000
        },
        {
          type: jsPsychSurvey,
          survey_json: function() {
            // This is a function that dynamically creates the JSON configuration for each trial.
            // Inside the function, you can access timeline variables, jsPsych data, and other global variables
            // that you define. This function will be called right before the trial starts.
            const this_trial_json = { 
              elements: [
                {
                  type: "text", 
                  title: `Enter a word related to ${jsPsych.timelineVariable('word').toUpperCase()}:`, 
                  autocomplete: "off" 
                }
              ], 
              showQuestionNumbers: false, 
              completeText: "Next", 
              focusFirstQuestionAutomatic: true 
            }
            return this_trial_json;
          },
          data: { word: jsPsych.timelineVariable('word') }
        }
      ],
      timeline_variables: [
        { word: 'cheese' },
        { word: 'ring' },
        { word: 'bat' },
        { word: 'cow' }
      ]
    };
    
  3. Use the survey_function parameter. You can reference trial-level variables from inside this function, so the general idea here is the same as the example above. The difference is that here you are using SurveyJS's JavaScript API syntax to create the survey content, instead of the JSON configuration. Again, this could be done without timeline variables, but this example uses timeline variables for convenience:

    const create_word_survey = (survey) => {
      // Create question using timeline variables
      const page = survey.addNewPage('page1');
      const question = page.addNewQuestion('text'); 
      question.title = `Enter a word related to ${jsPsych.timelineVariable('word').toUpperCase()}`;
      question.autocomplete = "off";
      // Set survey-level parameters
      survey.showQuestionNumbers = false;
      survey.completeText = "Next";
      survey.focusFirstQuestionAutomatic = true;
    }
    
    const word_trials = {
      timeline: [
        {
          type: jsPsychHtmlKeyboardResponse,
          stimulus: '+',
          choices: "NO_KEYS",
          trial_duration: 500
        },
        {
          type: jsPsychHtmlKeyboardResponse,
          stimulus: jsPsych.timelineVariable('word'),
          choices: "NO_KEYS",
          trial_duration: 1000
        },
        {
          type: jsPsychSurvey,
          survey_function: create_word_survey,
          data: { word: jsPsych.timelineVariable('word') }
        }
      ],
      timeline_variables: [
        { word: 'cheese' },
        { word: 'ring' },
        { word: 'bat' },
        { word: 'cow' }
      ]
    };