17  Dynamic Parameters

learning goals
  1. Replace static parameters with dynamic functions that execute at trial runtime
  2. Access previous trial data to create adaptive feedback and conditional logic
  3. Build complex HTML displays using timeline variables within functions
  4. Apply dynamic parameters to multiple trial components (stimulus, duration, choices, prompts)
  5. Save dynamically generated parameters for complete data collection

17.1 Introduction

So far, we’ve learned how to use jsPsych.timelineVariable() to systematically vary parameters across trials. This approach works well when you know all the values you want to use ahead of time and can list them in your timeline variables.

However, sometimes you need parameters to change based on what happens during the experiment. For example:

  • Showing “Correct!” or “Incorrect!” feedback (you don’t know which until the participant responds)
  • Adjusting difficulty based on performance
  • Changing stimulus location based on experimental conditions

Dynamic parameters allow values to change based on events that occur during the experiment. The key insight is simple: replace any static parameter with a function that calculates the appropriate value right before each trial runs.

17.2 Reviewing jsPsych.timelineVariable("variable")

Before exploring dynamic parameters, let’s review timeline variables. Timeline variables allow you to systematically vary parameters by pre-defining all values, which jsPsych can then randomize and insert as needed. This works perfectly for switching stimuli across trials.

You can use jsPsych.timelineVariable() to replace most trial parameters. Here are common examples:

17.2.1 Varying Stimulus

This example should look familiar. Here, we are changing the stimulus HTML on the basis of what’s in the timeline variables. Using jsPsych.timelineVariable('word') will take what is in the word variable on any give trial and insert that into thestimulus parameter.

let word_trials = {
  timeline: [
    {
      type: jsPsychHtmlKeyboardResponse,
      stimulus: jsPsych.timelineVariable('word'),
      choices: ['f', 'j']
    }
  ],
  timeline_variables: [
    {word: '<p>CAT</p>'},
    {word: '<p>DOG</p>'},
    {word: '<p>BIRD</p>'}
  ]
};

17.2.2 Varying Multiple Parameters

You can extend this logic to substitute any parameter. The key requirement is providing the parameter in the correct format.

In this example, we’re changing the choices parameter to allow only the correct key on each trial. Notice that we still provide an array ([‘r’]) in our timeline variables. We’re also changing the trial_duration parameter to vary response time limits. Since this parameter expects a number, we provide numbers in our timeline variables.

let stroop = {
  timeline: [
    {
      type: jsPsychHtmlKeyboardResponse,
      stimulus: jsPsych.timelineVariable('stimulus'),
      choices: jsPsych.timelineVariable('valid_keys'),
      trial_duration: jsPsych.timelineVariable('time_limit')
    }
  ],
  timeline_variables: [
    {
      stimulus: '<p style="color: red;">RED</p>',
      valid_keys: ['r'],
      time_limit: 2000
    },
    {
      stimulus: '<p style="color: blue;">BLUE</p>',
      valid_keys: ['b'],
      time_limit: 1500
    }
  ]
};

Here, we’re changing the choices parameter on each trial to only allow the correct key. Notice how we still provide an array inside our timeline variables (['r']).

We are also changing our time_duration parameter on each trial to change how long they have to respond. This parameter expects a number, so we provide a number in our timeline variables.

17.2.3 Varying Data Labels

In the previous data chapter, we also used timeline variables to update our data values. This is the same principle:

let categorization_trials = {
  timeline: [
    {
      type: jsPsychImageKeyboardResponse,
      stimulus: jsPsych.timelineVariable('image'),
      choices: ['f', 'j'],
      data: {
        category: jsPsych.timelineVariable('category'),
        correct_response: jsPsych.timelineVariable('correct_key')
      }
    }
  ],
  timeline_variables: [
    {image: 'cat.jpg', category: 'animal', correct_key: 'f'},
    {image: 'car.jpg', category: 'vehicle', correct_key: 'j'},
    {image: 'dog.jpg', category: 'animal', correct_key: 'f'}
  ]
};

17.2.4 What Timeline Variables Can’t Do

Timeline variables have an important limitation: all values must be determined before the experiment begins. This means you cannot:

  • Use conditional logic (“show this stimulus IF something happened”)
  • Use data from previous trials to change displays
  • Provide real-time feedback like “Correct!” vs “Incorrect!”
  • Make real-time calculations (“make the next trial easier IF they performed poorly”)

This is where dynamic parameters (functions) become important!

17.3 Functions as Parameters: The Basic Idea

A function is code that runs when called. In jsPsych, when you use a function as a parameter, jsPsych automatically calls that function right before the trial starts and uses the returned value as the parameter.

let trial = {
  type: jsPsychHtmlKeyboardResponse,
  stimulus: function() {
    // This code runs right before the trial starts
    let output = '<p>Hello!</p>';
    return output;
  }
}

This might seem unnecessary now, but the power comes from running code inside the function to decide what to return.

When declaring a function, follow this syntax:

let myFunction = function(){
    // function code goes inside here
}

The function runs when ‘called’ (e.g., myFunction()). To save results, use return to send a value back. We typically create a variable called output and return its contents. This becomes our trial parameter.

When using functions as jsPsych parameters, don’t call the function yourself. We provide the function to jsPsych, and it will call the function when needed:

stimulus: function() {
  let output = '<p>Hello!</p>';
  return output;
}

Notice there are no parentheses after function() at the end. We’re giving jsPsych the function itself, not calling it. jsPsych will call it for us when it’s time to run the trial.

You can make almost any trial parameter dynamic. We’ll focus mostly on the stimulus parameter using jsPsychHtmlKeyboardResponse since this creates the most useful complex displays. The same principles apply to other parameters like trial_duration and choices.

An important requirement, however, is that your function must return the format jsPsych expects: - stimulus: return a string (HTML text) - trial_duration: return a number (milliseconds) - choices: return an array of strings (like [‘f’, ‘j’])

Understanding what format each parameter expects will help you avoid common errors.

17.4 Two Critical Rules for Dynamic Parameters

There are two important principles we need to remember if we want to use dynamic parameters.

17.4.1 1. Timeline Variable Syntax Changes

When using timeline variables inside functions, the syntax changes:

  • Inside functions, use: jsPsych.evaluateTimelineVariable('variableName')
  • Outside functions, use: jsPsych.timelineVariable('variableName')

Use .evaluateTimelineVariable() to immediately get the value, while .timelineVariable() creates a placeholder for later evaluation.

17.4.2 2. Save Dynamic Parameters

Dynamic parameters aren’t automatically saved to your data. Use save_trial_parameters to specify which ones to save:

let trial = {
  timeline: [
    {
      type: jsPsychKeyboardResponse,
      stimulus: jsPsych.timelineVariable("color"),
      choices: ["r"],
      trial_duration: 1000
    }
  ],
  timeline_variables: [
    {color: "RED"},
    {color: "BLUE"}
  ],
  save_trial_parameters: {
    choices: true,
    trial_duration: true
  }
}

17.5 Dynamic stimulus

17.5.1 A Basic Example

Let’s start with something familiar. Remember how we used timeline variables to create Stroop stimuli? We might have done something like this:

let stroop = {
  type: jsPsychHtmlKeyboardResponse,
  timeline: [
    {
      stimulus: jsPsych.timelineVariable('stimulus')
    }
  ],
  timeline_variables: [
    {stimulus: '<p style="color: red;">RED</p>'},
    {stimulus: '<p style="color: blue;">BLUE</p>'},
    {stimulus: '<p style="color: green;">GREEN</p>'}
  ]
}

But what if we wanted more control over how the HTML is built? We can use a function to access timeline variables and construct HTML with more control:

let stroop = {
    type: jsPsychHtmlKeyboardResponse,
    timeline: [
        {
            stimulus: function() {
                // Get the timeline variables for this trial
                // For convenience, we'll store them inside variables called 'word' and 'color'
                let word = jsPsych.evaluateTimelineVariable('word');
                let color = jsPsych.evaluateTimelineVariable('color');

                // Build the HTML using these variables
                let output = `<p style='color: ${color}; font-size: 48px;'>${word}</p>`;
                
                // Important: We must return the output variable
                return output;
            }
        }
    ],
    timeline_variables: [
        {word: 'RED', color: 'red'},
        {word: 'BLUE', color: 'blue'},
        {word: 'GREEN', color: 'green'},
        {word: 'RED', color: 'blue'},  // Incongruent
        {word: 'BLUE', color: 'green'}, // Incongruent
        {word: 'GREEN', color: 'red'} // Incongruent
    ]
}

We’ve replaced our stimulus with a function that constructs HTML and returns it via the output variable.

Notice we’re using template literals (backticks) and ${variable} syntax to insert variables into HTML strings. This is cleaner than concatenating with +.

We can expand on this example and change other aspects of the HTML display using the same logic. For instance, we can also update the font size on every trial:

let stroop = {
  type: jsPsychHtmlKeyboardResponse,
  timeline: [
    {
      stimulus: function() {
        // Get the timeline variables for this trial
        let word = jsPsych.evaluateTimelineVariable('word');
        let color = jsPsych.evaluateTimelineVariable('color');
        let fontSize = jsPsych.evaluateTimelineVariable('fontSize');

         // Build the HTML using these variables
         let output = `<p style='color: ${color}; font-size: ${fontSize}px;'>${word}</p>`;
         return output;
      }
    }
  ],
  timeline_variables: [
    {word: 'RED', color: 'red', fontSize: '24'},
    {word: 'BLUE', color: 'blue', fontSize: '36'},
    {word: 'GREEN', color: 'green', fontSize: '48'},
    {word: 'RED', color: 'blue', fontSize: '24'},  // Incongruent
    {word: 'BLUE', color: 'green', fontSize: '36'}, // Incongruent
    {word: 'GREEN', color: 'red', fontSize: '48'} // Incongruent
  ]
}

17.5.2 Adding Logic: Changing the Difficulty

So far, we haven’t done anything we couldn’t do without the function. Though, in my opinion this formatting is much cleaner: We can clearly see the variables that we’re changing on each trial and our timeline_variables are not cluttered with extra HTML.

The real advantage of functions is introducing complex logic. Let’s make some trials harder by repeating the Stroop stimulus with distractors.

Let’s try something new with our Stroop stimuli. What if we wanted to make the task harder on some trials? Maybe by repeating the Stroop stimulus multiple times with additional distractors?

We need if-else logic: IF a trial is “hard” THEN display the word three times. IF “easy” THEN display once.

Let’s see how we can accomplish that:

let stroop = {
  type: jsPsychHtmlKeyboardResponse,
  timeline: [
    {
      stimulus: function() {
        // Get the timeline variables for this trial
        let word = jsPsych.evaluateTimelineVariable('word');
        let color = jsPsych.evaluateTimelineVariable('color');
        let difficulty = jsPsych.evaluateTimelineVariable('difficulty');
        
        // set an empty variable for our output
        let output 
        
        // change our output HTML depending on what 'difficulty' is
        if(difficulty === "hard"){
          output = `<p style="color:black; font-size: 36pt">${word}</p>
                    <p style="color:${color}; font-size: 36pt">${word}</p>
                    <p style="color:black; font-size: 36pt">${word}</p>
                    `;
        
        } else if(difficulty === "easy"){
          output = `<p style="color:${color}; font-size: 36pt">${word}</p>
                    `;
        
        }

        // we'll return what is in output
        return output
      }
    }
  ],
  timeline_variables: [
    {word: 'RED', color: 'red', difficulty: 'hard'},
    {word: 'BLUE', color: 'blue', difficulty: 'hard'},
    {word: 'RED', color: 'blue',  difficulty: 'hard'},
    {word: 'BLUE', color: 'red',  difficulty: 'hard'},  
    
    {word: 'RED', color: 'red', difficulty: 'easy'},
    {word: 'BLUE', color: 'blue', difficulty: 'easy'},
    {word: 'RED', color: 'blue',  difficulty: 'easy'},
    {word: 'BLUE', color: 'red',  difficulty: 'easy'}  
  ],
  randomize_order: true
}

In the function I’ve added some if-else logic to define what the HTML is going to be. Now I’m not inserting the difficulty timeline variable. Instead, I’m checking what it is, and changing the display depending on whether it is set to ‘hard’ or ‘easy’.

We can add more conditions if we wanted to this logic. For example, we can make this even more complex by adding a different kind of ‘neutral’ trial that presents “XXXX” in place of the flanking distractors.

let stroop = {
  type: jsPsychHtmlKeyboardResponse,
  timeline: [
    {
      stimulus: function() {
        // Get the timeline variables for this trial
        let word = jsPsych.evaluateTimelineVariable('word');
        let color = jsPsych.evaluateTimelineVariable('color');
        let difficulty = jsPsych.evaluateTimelineVariable('difficulty');
        
        // set an empty variable for our output
        let output 
        
        // change our output HTML depending on what 'difficulty' is
        if(difficulty === "hard"){
          output = `<p style="color:black; font-size: 36pt">${word}</p>
                    <p style="color:${color}; font-size: 36pt">${word}</p>
                    <p style="color:black; font-size: 36pt">${word}</p>
                    `;
        
        } else if(difficulty === "easy"){
          output = `<p style="color:${color}; font-size: 36pt">${word}</p>
                    `;
        
        } else if(difficulty === "neutral"){
           output = `<p style="color:black; font-size: 36pt">XXXX</p>
                    <p style="color:${color}; font-size: 36pt">${word}</p>
                    <p style="color:black; font-size: 36pt">XXXX</p>
                    `;
        
        }

        // we'll return what is in output
        return output
      }
    }
  ],
  timeline_variables: [
    {word: 'RED', color: 'red', difficulty: 'hard'},
    {word: 'BLUE', color: 'blue', difficulty: 'hard'},
    {word: 'RED', color: 'blue',  difficulty: 'hard'},
    {word: 'BLUE', color: 'red',  difficulty: 'hard'},  
    
    {word: 'RED', color: 'red', difficulty: 'easy'},
    {word: 'BLUE', color: 'blue', difficulty: 'easy'},
    {word: 'RED', color: 'blue',  difficulty: 'easy'},
    {word: 'BLUE', color: 'red',  difficulty: 'easy'},  
    
    {word: 'RED', color: 'red', difficulty: 'neutral'},
    {word: 'BLUE', color: 'blue', difficulty: 'neutral'},
    {word: 'RED', color: 'blue',  difficulty: 'neutral'},
    {word: 'BLUE', color: 'red',  difficulty: 'neutral'} 
  ],
  randomize_order: true
}

And let’s see a full example using that code:

 <!DOCTYPE html>
<html>
<head>
    <title>Recognition DRM</title>
    <!-- jsPsych -->
    <script src="jspsych/jspsych.js"></script>
    <link href="jspsych/jspsych.css" rel="stylesheet" type="text/css" />
    
    <!-- jPsych plugins -->
    <script src="jspsych/plugin-html-keyboard-response.js"></script>

    <!-- custom CSS -->
     <link href="style.css" rel="stylesheet" type="text/css" />
</head>
<body>
  <!-- custom JS -->
  <script src="exp.js"></script>
</body>
</html>
 
 // 1. Initialize jsPsych
const jsPsych = initJsPsych();

 
// 2. Define our trials
const welcome = {
  type: jsPsychHtmlKeyboardResponse,
  stimulus: `Welcome to the Experiment! Press any key to begin.`,
  choices: "ALL_KEYS", 
  post_trial_gap: 250
}

let stroop = {
  type: jsPsychHtmlKeyboardResponse,
  timeline: [
    {
      stimulus: `<p style="font-size: 48pt">+</p>`,
      choices: "NO_KEYS",
      trial_duration: 750,
      post_trial_gap: 250
    },
    {
      stimulus: function() {
        // Get the timeline variables for this trial
        let word = jsPsych.evaluateTimelineVariable("word");
        let color = jsPsych.evaluateTimelineVariable("color");
        let difficulty = jsPsych.evaluateTimelineVariable("difficulty");
        
        // set an empty variable for our output
        let output 
        
        // change our output HTML depending on what "difficulty" is
        if(difficulty === "hard"){
          output = `<p style="color:black;; font-size: 36pt">${word}</p>
                    <p style="color:${color}; font-size: 36pt">${word}</p>
                    <p style="color:black;; font-size: 36pt">${word}</p>
                    `;
        
        } else if(difficulty === "easy"){
          output = `<p style="color:${color}; font-size: 36pt">${word}</p>
                    `;
        
        } else if(difficulty === "neutral"){
           output = `<p style="color:black; font-size: 36pt">XXXX</p>
                    <p style="color:${color}; font-size: 36pt">${word}</p>
                    <p style="color:black; font-size: 36pt">XXXX</p>
                    `;
        
        }

        // we"ll return what is in output
        return output
      }
    }
  ],
  timeline_variables: [
    {word: "RED", color: "red", difficulty: "hard"},
    {word: "BLUE", color: "blue", difficulty: "hard"},
    {word: "RED", color: "blue",  difficulty: "hard"},
    {word: "BLUE", color: "red",  difficulty: "hard"},  
    
    {word: "RED", color: "red", difficulty: "easy"},
    {word: "BLUE", color: "blue", difficulty: "easy"},
    {word: "RED", color: "blue",  difficulty: "easy"},
    {word: "BLUE", color: "red",  difficulty: "easy"},  
    
    {word: "RED", color: "red", difficulty: "neutral"},
    {word: "BLUE", color: "blue", difficulty: "neutral"},
    {word: "RED", color: "blue",  difficulty: "neutral"},
    {word: "BLUE", color: "red",  difficulty: "neutral"} 
  ],
  choices: ["b","r"],
  randomize_order: true,
  post_trial_gap: 250
}

// 3. Run jsPsych with our trials
jsPsych.run([
  welcome,
  stroop
]); 
  
Live JsPsych Demo Click inside the demo to activate demo

17.5.3 Adding Logic: Changing the Location

I’m hoping you can see how this opens up unlimited possibilities!

Let’s change stimulus location using more complex HTML. I’m using ‘flexbox’ positioning, which provides convenient ways to control alignment in rows, columns, and centering.

Let’s look at how we can accomplish a stimulus display that positions the stimulus on the left versus right side of the display:

let simon = {
  type: jsPsychHtmlKeyboardResponse,
  timeline: [
    {
      stimulus: function() {
        // Get the timeline variables for this trial
        let word = jsPsych.evaluateTimelineVariable('word');
        let location = jsPsych.evaluateTimelineVariable('location');
        
        // set an empty variable for our output
        let output 
        
        if (location === "left") {
        
            output = `
              <div style="display: flex; width: 600px; height: 200px; align-items: center;">
                  <div style="flex: 1; text-align: center; font-size: 36px;">${word}</div>
                  <div style="flex: 1;"></div>
              </div>
          `;
        } else if (location === "right") {
        
            output = `
                <div style="display: flex; width: 600px; height: 200px; align-items: center;">
                    <div style="flex: 1;"></div>
                    <div style="flex: 1; text-align: center; font-size: 36px;">${word}</div>
                </div>
            `;
        }

        // we'll return what is in output
        return output
      }
    }
  ],
  timeline_variables: [
    {word: 'LEFT', location: "left"},
    {word: 'RIGHT', location: "left"},
    {word: 'LEFT', location: "right"},
    {word: 'RIGHT', location: "right"}
  ],
  randomize_order: true
}

And again, just for kicks, let’s expand that to include a third, center, location. Maybe this would be considered a ‘neutral’ location for this kind of experiment.

Because I used the flexbox method, this is pretty easy to just add a third location.

let simon = {
  type: jsPsychHtmlKeyboardResponse,
  timeline: [
    {
      stimulus: function() {
        // Get the timeline variables for this trial
        let word = jsPsych.evaluateTimelineVariable('word');
        let location = jsPsych.evaluateTimelineVariable('location');
        
        // set an empty variable for our output
        let output 
        
        if (location === "left") {
        
            output = `
              <div style="display: flex; width: 600px; height: 200px; align-items: center;">
                  <div style="flex: 1; text-align: center; font-size: 36px;">${word}</div>
                  <div style="flex: 1;"></div>
                  <div style="flex: 1;"></div>
              </div>
          `;
        } else if (location === "right") {
        
            output = `
                <div style="display: flex; width: 600px; height: 200px; align-items: center;">
                    <div style="flex: 1;"></div>
                    <div style="flex: 1;"></div>
                    <div style="flex: 1; text-align: center; font-size: 36px;">${word}</div>
                </div>
            `;
        } else if (location === "center") {
        
            output = `
                <div style="display: flex; width: 600px; height: 200px; align-items: center;">
                    <div style="flex: 1;"></div>
                    <div style="flex: 1; text-align: center; font-size: 36px;">${word}</div>
                    <div style="flex: 1;"></div>
                </div>
            `;
        }

        // we'll return what is in output
        return output
      }
    }
  ],
  timeline_variables: [
    {word: 'LEFT', location: "left"},
    {word: 'RIGHT', location: "left"},
    {word: 'LEFT', location: "right"},
    {word: 'RIGHT', location: "right"},
    {word: 'LEFT', location: "center"},
    {word: 'RIGHT', location: "center"},
  ],
  randomize_order: true
}

Here’s a full example using that code:

 <!DOCTYPE html>
<html>
<head>
    <title>Simon Task</title>
    <!-- jsPsych -->
    <script src="jspsych/jspsych.js"></script>
    <link href="jspsych/jspsych.css" rel="stylesheet" type="text/css" />
    
    <!-- jPsych plugins -->
    <script src="jspsych/plugin-html-keyboard-response.js"></script>

    <!-- custom CSS -->
     <link href="style.css" rel="stylesheet" type="text/css" />
</head>
<body>
  <!-- custom JS -->
  <script src="exp.js"></script>
</body>
</html>
 
 // 1. Initialize jsPsych
const jsPsych = initJsPsych();

 
// 2. Define our trials
const welcome = {
  type: jsPsychHtmlKeyboardResponse,
  stimulus: `Welcome to the Experiment! Press "Z" if the word says "LEFT" or "M" if the word says "RIGHT". Press any key to begin.`,
  choices: "ALL_KEYS", 
  post_trial_gap: 250
}

let simon = {
  type: jsPsychHtmlKeyboardResponse,
  timeline: [
    {
      stimulus: `<p style="font-size: 48pt">+</p>`,
      choices: "NO_KEYS",
      trial_duration: 750,
      post_trial_gap: 250
    },
    {
      stimulus: function() {
        // Get the timeline variables for this trial
        let word = jsPsych.evaluateTimelineVariable("word");
        let location = jsPsych.evaluateTimelineVariable("location");
        
        // set an empty variable for our output
        let output 
        
        if (location === "left") {
        
            output = `
              <div style="display: flex; width: 600px; height: 200px; align-items: center;">
                  <div style="flex: 1; text-align: center; font-size: 36px;">${word}</div>
                  <div style="flex: 1;"></div>
                  <div style="flex: 1;"></div>
              </div>
          `;
        } else if (location === "right") {
        
            output = `
                <div style="display: flex; width: 600px; height: 200px; align-items: center;">
                    <div style="flex: 1;"></div>
                    <div style="flex: 1;"></div>
                    <div style="flex: 1; text-align: center; font-size: 36px;">${word}</div>
                </div>
            `;
        } else if (location === "center") {
        
            output = `
                <div style="display: flex; width: 600px; height: 200px; align-items: center;">
                    <div style="flex: 1;"></div>
                    <div style="flex: 1; text-align: center; font-size: 36px;">${word}</div>
                    <div style="flex: 1;"></div>
                </div>
            `;
        }

        // we"ll return what is in output
        return output
      },
      choices: ["z","m"],
      post_trial_gap: 250
    }
  ],
  timeline_variables: [
    {word: "LEFT", location: "left"},
    {word: "RIGHT", location: "left"},
    {word: "LEFT", location: "right"},
    {word: "RIGHT", location: "right"},
    {word: "LEFT", location: "center"},
    {word: "RIGHT", location: "center"}
  ],
  randomize_order: true
}

// 3. Run jsPsych with our trials
jsPsych.run([
  welcome,
  simon
]); 
  
Live JsPsych Demo Click inside the demo to activate demo

I want to point out that creating these displays with HTML and CSS takes some trial-and-error. It’s unlikely that you’ll write it exactly correct the first time. Often I will add styling with only a rough idea about how it will look, then fine tune it until I’m happy with it.

One strategy for speeding up the trial-and-error is opening the experiment, going to a trial, then inspecting it in the browser. From there, you can click on an element and see the CSS being applied to that element. You can also modify it right in the browser! That means, I could click on one of the divs and add properties like border: solid. This will update how it looks right now in the browser, but won’t change anything permanently in your experiment. You can try different CSS styling until you’re happy with what you see, then go update your experiment code with the styling you want.

In fact, that’s what I did while coding this demo. I went through some trial-and-error changing how wide the display was, how tall, adding (then removing) a border around the boxes, etc. This is part of the process.

How about one more example?

For this one, I’ll use the grid method for creating the display to create different spatial locations for arrows to be placed. I had to do some creative problem-solving for this one because adding post_tria_gap: 250 causes the squares to disappear, when I’d like them to be present the whole time. So I had to make my own “blank” screens with just the grid and nothing placed in them to create a similar effect.

 <!DOCTYPE html>
<html>
<head>
    <title>Simon Task</title>
    <!-- jsPsych -->
    <script src="jspsych/jspsych.js"></script>
    <link href="jspsych/jspsych.css" rel="stylesheet" type="text/css" />
    
    <!-- jPsych plugins -->
    <script src="jspsych/plugin-html-keyboard-response.js"></script>

    <!-- custom CSS -->
     <link href="style.css" rel="stylesheet" type="text/css" />
</head>
<body>
  <!-- custom JS -->
  <script src="exp.js"></script>
</body>
</html>
 
 // 1. Initialize jsPsych
const jsPsych = initJsPsych();

 
// 2. Define our trials
const welcome = {
  type: jsPsychHtmlKeyboardResponse,
  stimulus: `<p>Welcome to the Arrow Experiment!</p>
              <p>Press the key that matches the arrow's location: Q (top-left), E (top-right), Z (bottom-left), or C (bottom-right).</p>
              <p>Press any key to begin.</p>`,
  choices: "ALL_KEYS", 
  post_trial_gap: 250
}

let simon = {
  type: jsPsychHtmlKeyboardResponse,
  timeline: [
    // this display is the fixation
    {
      stimulus: ` <div style="display: grid; grid-template-columns: 1fr 1fr 1fr; grid-template-rows: 1fr 1fr 1fr; width: 300px; height: 300px; gap: 10px;">
                  <div style="border: 2px solid black;"></div>
                  <div></div>
                  <div style="border: 2px solid black;"></div>
                  <div></div>
                  <div style="display: flex; align-items: center; justify-content: center; font-size: 48px;">+</div>
                  <div></div>
                  <div style="border: 2px solid black;"></div>
                  <div></div>
                  <div style="border: 2px solid black;"></div>
              </div>`,
      choices: "NO_KEYS",
      trial_duration: 750
    },

    // this display is blank 250 ms
   {
        stimulus: ` <div style="display: grid; grid-template-columns: 1fr 1fr 1fr; grid-template-rows: 1fr 1fr 1fr; width: 300px; height: 300px; gap: 10px;">
                    <div style="border: 2px solid black;"></div>
                    <div></div>
                    <div style="display: flex; align-items: center; justify-content: center; font-size: 48px; border: 2px solid black;"></div>
                    <div></div>
                    <div></div>
                    <div></div>
                    <div style="border: 2px solid black;"></div>
                    <div></div>
                    <div style="border: 2px solid black;"></div>
                </div>`,
        choices: "NO_KEYS",
        trial_duration: 250
      },

     // target display until response
    {
      stimulus: function() {
        // Get the timeline variables for this trial
        let arrow = jsPsych.evaluateTimelineVariable("arrow");
        let location = jsPsych.evaluateTimelineVariable("location");
        
        // set an empty variable for our output
        let output 
        
        if (location === "top-left") {
            output = `
              <div style="display: grid; grid-template-columns: 1fr 1fr 1fr; grid-template-rows: 1fr 1fr 1fr; width: 300px; height: 300px; gap: 10px;">
                  <div style="border: 2px solid black;"></div>
                  <div></div>
                  <div style="display: flex; align-items: center; justify-content: center; font-size: 48px; border: 2px solid black;">${arrow}</div>
                  <div></div>
                  <div></div>
                  <div></div>
                  <div style="border: 2px solid black;"></div>
                  <div></div>
                  <div style="border: 2px solid black;"></div>
              </div>
            `;
        } else if (location === "top-right") {
            output = `
              <div style="display: grid; grid-template-columns: 1fr 1fr 1fr; grid-template-rows: 1fr 1fr 1fr; width: 300px; height: 300px; gap: 10px;">
                  <div style="border: 2px solid black;"></div>
                  <div></div>
                  <div style="display: flex; align-items: center; justify-content: center; font-size: 48px; border: 2px solid black;">${arrow}</div>
                  <div></div>
                  <div></div>
                  <div></div>
                  <div style="border: 2px solid black;"></div>
                  <div></div>
                  <div style="border: 2px solid black;"></div>
              </div>
            `;
        } else if (location === "bottom-left") {
            output = `
              <div style="display: grid; grid-template-columns: 1fr 1fr 1fr; grid-template-rows: 1fr 1fr 1fr; width: 300px; height: 300px; gap: 10px;">
                  <div style="border: 2px solid black;"></div>
                  <div></div>
                  <div style="border: 2px solid black;"></div>
                  <div></div>
                  <div></div>
                  <div></div>
                  <div style="display: flex; align-items: center; justify-content: center; font-size: 48px; border: 2px solid black;">${arrow}</div>
                  <div></div>
                  <div style="border: 2px solid black;"></div>
              </div>
            `;
        } else if (location === "bottom-right") {
            output = `
              <div style="display: grid; grid-template-columns: 1fr 1fr 1fr; grid-template-rows: 1fr 1fr 1fr; width: 300px; height: 300px; gap: 10px;">
                  <div style="border: 2px solid black;"></div>
                  <div></div>
                  <div style="border: 2px solid black;"></div>
                  <div></div>
                  <div></div>
                  <div></div>
                  <div style="border: 2px solid black;"></div>
                  <div></div>
                  <div style="display: flex; align-items: center; justify-content: center; font-size: 48px; border: 2px solid black;">${arrow}</div>
              </div>
            `;
        }

        // we"ll return what is in output
        return output
      },
      choices: ["q","e", "z", "c"]
    },

    // this display is blank 250 ms
   {
        stimulus: ` <div style="display: grid; grid-template-columns: 1fr 1fr 1fr; grid-template-rows: 1fr 1fr 1fr; width: 300px; height: 300px; gap: 10px;">
                    <div style="border: 2px solid black;"></div>
                    <div></div>
                    <div style="display: flex; align-items: center; justify-content: center; font-size: 48px; border: 2px solid black;"></div>
                    <div></div>
                    <div></div>
                    <div></div>
                    <div style="border: 2px solid black;"></div>
                    <div></div>
                    <div style="border: 2px solid black;"></div>
                </div>`,
        choices: "NO_KEYS",
        trial_duration: 250
      }
  ],
  timeline_variables: [
    {arrow: "↖", location: "top-left"},
    {arrow: "↗", location: "top-left"},
    {arrow: "↙", location: "top-left"},
    {arrow: "↘", location: "top-left"},

    {arrow: "↖", location: "top-right"},
    {arrow: "↗", location: "top-right"},
    {arrow: "↙", location: "top-right"},
    {arrow: "↘", location: "top-right"},

    {arrow: "↖", location: "bottom-left"},
    {arrow: "↗", location: "bottom-left"},
    {arrow: "↙", location: "bottom-left"},
    {arrow: "↘", location: "bottom-left"},

    {arrow: "↖", location: "bottom-right"},
    {arrow: "↗", location: "bottom-right"},
    {arrow: "↙", location: "bottom-right"},
    {arrow: "↘", location: "bottom-right"}
  ],
  randomize_order: true
}

// 3. Run jsPsych with our trials
jsPsych.run([
  welcome,
  simon
]); 
  
Live JsPsych Demo Click inside the demo to activate demo

17.5.4 Adding Logic: Presenting Feedback

Returning to the idea that functions make our stimulus dynamic: this means we can change things on-the-fly based on what happened in previous trials.

This flexibility is useful in many ways, but the most common use case is adaptive feedback where we tell participants how they performed on the previous trial.

We already covered how to access data from previous trials in the last chapter. Now we’ll combine that knowledge with dynamic stimulus functions to create different kinds of performance feedback like accuracy messages and response time alerts.

17.5.4.1 Simple Accuracy

Let’s start with the simplest kind of accuracy feedback, which simply tells participants whether they were correct or incorrect.

I’ll do this in a couple of steps to make it clear what is happening. First, to make it easy to find, we’ll first store the accuracy in our data using the on_finish parameter (we learned about this in the previous chapter).

let stroop = {
    type: jsPsychHtmlKeyboardResponse,
    timeline: [
        {
            stimulus: function() {
                // Get the timeline variables for this trial
                // For convenience, we'll store them inside variables called 'word' and 'color'
                let word = jsPsych.evaluateTimelineVariable('word');
                let color = jsPsych.evaluateTimelineVariable('color');

                // Build the HTML using these variables
                let output = `<p style='color: ${color}; font-size: 48px;'>${word}</p>`;
                
                // Important: We must return the output variable
                return output;
            },
            choices: ["r", "b"],
            on_finish: function(data) {
                // First, determine accuracy for this trial
                data.correct = jsPsych.pluginAPI.compareKeys(data.response, data.correct_response);
            }
        }
    ],
    timeline_variables: [
        {word: 'RED', color: 'red', correct_response: "r"},
        {word: 'BLUE', color: 'red', correct_response: "r"},
        {word: 'RED', color: 'blue', correct_response: "b"},
        {word: 'BLUE', color: 'blue', correct_response: "b"},
    ]
}

Now at the end of each trial there will be a data parameter called correct with a value of true or false.

To add feedback, we add another trial component like we always do. BUT now we have to check what the accuracy was on the previous trial. Remember: jsPsych treats every event that happens as a ‘trial’ and stores a row of data for it. That means on the feedback trial, we need to look back one trial to see the accuracy and change the feedback depending on accuracy.

let stroop = {
    type: jsPsychHtmlKeyboardResponse,
    timeline: [
        {
            stimulus: function() {
                // Get the timeline variables for this trial
                // For convenience, we will store them inside variables called "word" and "color"
                let word = jsPsych.evaluateTimelineVariable("word");
                let color = jsPsych.evaluateTimelineVariable("color");

                // Build the HTML using these variables
                let output = `<p style="color: ${color}; font-size: 48px;">${word}</p>`;
                
                // Important: We must return the output variable
                return output;
            },
            choices: ["r", "b"],
            on_finish: function(data) {
                // First, determine accuracy for this trial
                data.correct = jsPsych.pluginAPI.compareKeys(data.response, data.correct_response);
            }
        },
        // This is our feedback trial
        {
          stimulus: function() {
              let output
          
              // get all previous data
              let all_data = jsPsych.data.get();
              
              // filter it to get the last trial
              let last_trial = all_data.last(1).trials

              console.log(last_trial)
              
              // look at the `correct` parameter to get accuracy
              // note: filtering ALWAYS returns an array, even if it just has one thing in it
              let accuracy = last_trial[0].correct
              
              // check accuracy
              if(accuracy === true){
                  output = `<p style="font-size:36px; color:MediumSeaGreen">CORRECT!</p>`
              } else {
                  output = `<p style="font-size:36px; color:Tomato">INCORRECT!</p>`
              }
              
              return output
          },
          choices: "NO_KEYS",
          trial_duration: 1000
        }
    ],
    timeline_variables: [
        {word: "RED", color: "red", correct_response: "r"},
        {word: "BLUE", color: "red", correct_response: "r"},
        {word: "RED", color: "blue", correct_response: "b"},
        {word: "BLUE", color: "blue", correct_response: "b"}
    ],
    data: {
      word: jsPsych.timelineVariable("word"),
      color: jsPsych.timelineVariable("color"),
      correct_response: jsPsych.timelineVariable("correct_response")
    },
    randomize_order: true
}

Some important little ‘catches’ here that may throw you off. When we filter the data to only get the last trial, it returns an array of trials. In this case, the array only has one thing in it, the last trial, but we still have use the array reference to get the data inside it. That’s why I’ve used last_trial[0] to access the first thing in the array.

The nice thing is that this generalizes. So, if I had called let last_trials = all_data.last(2), that would get me the last two trials. In which case, I could access the data from two trials ago with last_trials[0] or the last trial with last_trials[1].

Now, let’s see this example in action:

 <!DOCTYPE html>
<html>
<head>
    <title>Simon Task</title>
    <!-- jsPsych -->
    <script src="jspsych/jspsych.js"></script>
    <link href="jspsych/jspsych.css" rel="stylesheet" type="text/css" />
    
    <!-- jPsych plugins -->
    <script src="jspsych/plugin-html-keyboard-response.js"></script>

    <!-- custom CSS -->
     <link href="style.css" rel="stylesheet" type="text/css" />
</head>
<body>
  <!-- custom JS -->
  <script src="exp.js"></script>
</body>
</html>
 
 // 1. Initialize jsPsych
const jsPsych = initJsPsych();

 
// 2. Define our trials
const welcome = {
  type: jsPsychHtmlKeyboardResponse,
  stimulus: `<p>Welcome to the Experiment!</p>
              <p>Press any key to begin.</p>`,
  choices: "ALL_KEYS", 
  post_trial_gap: 250
}

let stroop = {
    type: jsPsychHtmlKeyboardResponse,
    timeline: [
        {
            stimulus: function() {
                // Get the timeline variables for this trial
                // For convenience, we will store them inside variables called "word" and "color"
                let word = jsPsych.evaluateTimelineVariable("word");
                let color = jsPsych.evaluateTimelineVariable("color");

                // Build the HTML using these variables
                let output = `<p style="color: ${color}; font-size: 48px;">${word}</p>`;
                
                // Important: We must return the output variable
                return output;
            },
            choices: ["r", "b"],
            on_finish: function(data) {
                // First, determine accuracy for this trial
                data.correct = jsPsych.pluginAPI.compareKeys(data.response, data.correct_response);
            }
        },
        // This is our feedback trial
        {
          stimulus: function() {
              let output
          
              // get all previous data
              let all_data = jsPsych.data.get();
              
              // filter it to get the last trial
              let last_trial = all_data.last(1).trials

              console.log(last_trial)
              
              // look at the `correct` parameter to get accuracy
              // note: filtering ALWAYS returns an array, even if it just has one thing in it
              let accuracy = last_trial[0].correct
              
              // check accuracy
              if(accuracy === true){
                  output = `<p style="font-size:36px; color:MediumSeaGreen">CORRECT!</p>`
              } else {
                  output = `<p style="font-size:36px; color:Tomato">INCORRECT!</p>`
              }
              
              return output
          },
          choices: "NO_KEYS",
          trial_duration: 1000
        }
    ],
    timeline_variables: [
        {word: "RED", color: "red", correct_response: "r"},
        {word: "BLUE", color: "red", correct_response: "r"},
        {word: "RED", color: "blue", correct_response: "b"},
        {word: "BLUE", color: "blue", correct_response: "b"},
    ],
    data: {
      word: jsPsych.timelineVariable("word"),
      color: jsPsych.timelineVariable("color"),
      correct_response: jsPsych.timelineVariable("correct_response")
    },
    randomize_order: true
}

// 3. Run jsPsych with our trials
jsPsych.run([
  welcome,
  stroop
]); 
  
Live JsPsych Demo Click inside the demo to activate demo

17.5.4.2 Corrective Feedback

We can access all parts of data and make our feedback as comprehensive as we’d like. For instance, if we were providing practice trials, we might want to provide corrective feedback to make sure participants understood the instructions:

 <!DOCTYPE html>
<html>
<head>
    <title>Simon Task</title>
    <!-- jsPsych -->
    <script src="jspsych/jspsych.js"></script>
    <link href="jspsych/jspsych.css" rel="stylesheet" type="text/css" />
    
    <!-- jPsych plugins -->
    <script src="jspsych/plugin-html-keyboard-response.js"></script>

    <!-- custom CSS -->
     <link href="style.css" rel="stylesheet" type="text/css" />
</head>
<body>
  <!-- custom JS -->
  <script src="exp.js"></script>
</body>
</html>
 
 // 1. Initialize jsPsych
const jsPsych = initJsPsych();

 
// 2. Define our trials
const welcome = {
  type: jsPsychHtmlKeyboardResponse,
  stimulus: `<p>Welcome to the Experiment!</p>
              <p>Press any key to begin.</p>`,
  choices: "ALL_KEYS", 
  post_trial_gap: 250
}

let stroop = {
    type: jsPsychHtmlKeyboardResponse,
    timeline: [
        {
            stimulus: function() {
                // Get the timeline variables for this trial
                // For convenience, we will store them inside variables called "word" and "color"
                let word = jsPsych.evaluateTimelineVariable("word");
                let color = jsPsych.evaluateTimelineVariable("color");

                // Build the HTML using these variables
                let output = `<p style="color: ${color}; font-size: 48px;">${word}</p>`;
                
                // Important: We must return the output variable
                return output;
            },
            choices: ["r", "b"],
            on_finish: function(data) {
                // First, determine accuracy for this trial
                data.correct = jsPsych.pluginAPI.compareKeys(data.response, data.correct_response);
            }
        },
        // This is our feedback trial
        {
          stimulus: function() {
              let output
          
              // get all previous data
              let all_data = jsPsych.data.get();
              
              // filter it to get the last trial
              let last_trial = all_data.last(1).trials

              console.log(last_trial)
              
              // look at the `correct` parameter to get accuracy
              // note: filtering ALWAYS returns an array, even if it just has one thing in it
              let accuracy = last_trial[0].correct
              let word = last_trial[0].word
              let color = last_trial[0].color
              let correct_response = last_trial[0].correct_response

              // check accuracy
              if(accuracy === true){
                  output = `<p style="font-size:36px; color:MediumSeaGreen">CORRECT!</p>
                            <p>Press any key to continue</p>`
              } else {
                  output = `<p style="font-size:36px; color:Tomato">INCORRECT!</p>
                            <p>The correct response for <span style="color:${color}">${word}</span> was "${correct_response}"</p>
                            <p>Remember: respond to the <em>color</em> and not the word</p>
                            <p>Press any key to continue</p>`
              }
              
              return output
          },
          choices: "ALL_KEYS"
        }
    ],
    timeline_variables: [
        {word: "RED", color: "red", correct_response: "r"},
        {word: "BLUE", color: "red", correct_response: "r"},
        {word: "RED", color: "blue", correct_response: "b"},
        {word: "BLUE", color: "blue", correct_response: "b"},
    ],
    data: {
      word: jsPsych.timelineVariable("word"),
      color: jsPsych.timelineVariable("color"),
      correct_response: jsPsych.timelineVariable("correct_response")
    },
    randomize_order: true
}

// 3. Run jsPsych with our trials
jsPsych.run([
  welcome,
  stroop
]); 
  
Live JsPsych Demo Click inside the demo to activate demo

17.5.4.3 Reaction Time Feedback

We can of course provide any kind of feedback we’d like. If an experiment emphasizes speed and accuracy, you may want to say whether they were too slow. You can do that by accessing the rt data.

We’ll just need some more complicated logic to check both accuracy and speed, then change the display based on both. Here, I’ll first check whether it was too slow, and display that regardless of accuracy. If they weren’t too slow, then I’ll display their accuracy.

 <!DOCTYPE html>
<html>
<head>
    <title>Simon Task</title>
    <!-- jsPsych -->
    <script src="jspsych/jspsych.js"></script>
    <link href="jspsych/jspsych.css" rel="stylesheet" type="text/css" />
    
    <!-- jPsych plugins -->
    <script src="jspsych/plugin-html-keyboard-response.js"></script>

    <!-- custom CSS -->
     <link href="style.css" rel="stylesheet" type="text/css" />
</head>
<body>
  <!-- custom JS -->
  <script src="exp.js"></script>
</body>
</html>
 
 // 1. Initialize jsPsych
const jsPsych = initJsPsych();

 
// 2. Define our trials
const welcome = {
  type: jsPsychHtmlKeyboardResponse,
  stimulus: `<p>Welcome to the Experiment!</p>
              <p>Press any key to begin.</p>`,
  choices: "ALL_KEYS", 
  post_trial_gap: 250
}

let stroop = {
    type: jsPsychHtmlKeyboardResponse,
    timeline: [
        {
            stimulus: function() {
                // Get the timeline variables for this trial
                // For convenience, we will store them inside variables called "word" and "color"
                let word = jsPsych.evaluateTimelineVariable("word");
                let color = jsPsych.evaluateTimelineVariable("color");

                // Build the HTML using these variables
                let output = `<p style="color: ${color}; font-size: 48px;">${word}</p>`;
                
                // Important: We must return the output variable
                return output;
            },
            choices: ["r", "b"],
            on_finish: function(data) {
                // First, determine accuracy for this trial
                data.correct = jsPsych.pluginAPI.compareKeys(data.response, data.correct_response);
            }
        },
        // This is our feedback trial
        {
          stimulus: function() {
              let output
          
              // get all previous data
              let all_data = jsPsych.data.get();
              
              // filter it to get the last trial
              let last_trial = all_data.last(1).trials

              console.log(last_trial)
              
              // look at the `correct` parameter to get accuracy
              // note: filtering ALWAYS returns an array, even if it just has one thing in it
              let accuracy = last_trial[0].correct
              let rt = last_trial[0].rt

              // check rt first
              if(rt > 2000){
                  output = `<p style="font-size:36px; color:MediumOrchid">Too Slow! Respond faster!</p>`
              } else if(accuracy === true){
                  output = `<p style="font-size:36px; color:MediumSeaGreen">CORRECT!</p>`
              } else {
                  output = `<p style="font-size:36px; color:Tomato">INCORRECT!</p>`
              }
              
              return output
          },
          choices: "NO_KEYS",
          trial_duration: 1000
        }
    ],
    timeline_variables: [
        {word: "RED", color: "red", correct_response: "r"},
        {word: "BLUE", color: "red", correct_response: "r"},
        {word: "RED", color: "blue", correct_response: "b"},
        {word: "BLUE", color: "blue", correct_response: "b"},
    ],
    data: {
      word: jsPsych.timelineVariable("word"),
      color: jsPsych.timelineVariable("color"),
      correct_response: jsPsych.timelineVariable("correct_response")
    },
    randomize_order: true
}

// 3. Run jsPsych with our trials
jsPsych.run([
  welcome,
  stroop
]); 
  
Live JsPsych Demo Click inside the demo to activate demo

17.6 Other Parameters

The general principals you’ve learned throughout this chapter can be applied to more than just the stimulus parameter. In fact, most parameters can become dynamic by replacing it with a function.

Here are a couple more examples of replacing parameters with functions.

Remember: We need to save these trial parameters if we’re making them dynamic, otherwise we won’t know what they were on every trial!

17.6.1 Trial Duration

Sometimes, we want to make our experiment unpredictable in some ways, like randomizing the delay between trials. We could do this by randomizing the trial_duration of the fixation cross. This uses a generic math function that generates a number between X and Y:

let trial = {
  type: jsPsychKeyboardResponse,
  stimulus: `<p style="font-size:48px; color:red;">GREEN</p>`,
  trial_duration: function(){
    return Math.floor(Math.random() * (2000 - 1000 + 1) + 1000);
  },
  choices: ['r', 'b', 'g', 'y'],
  save_trial_parameters: {
    trial_duration: true
  }
}

Or I could randomly select one from a set:

let trial = {
  type: jsPsychKeyboardResponse,
  stimulus: `<p style="font-size:48px; color:red;">GREEN</p>`,
  trial_duration: function(){
    // set of durations
    let durations = [500,750,1000,1250,1500,1750,2000]
    
    // randomize the order of the array
    durations = jsPsych.randomization.shuffle(durations)
    
    // select the first one
    let selected_duration = durations[0]
  
    return selected_duration
  },
  choices: ['r', 'b', 'g', 'y'],
  save_trial_parameters: {
    trial_duration: true
  }
}

17.6.2 Choices

We could also make our choices dynamic. For instance, when we use the button plugin, we might want to randomize the order of choices, so that it differs on every trial. We could do that with a function.

We do need to keep in mind though, that the choice parameter expects an array [ ], so no matter what we do in the function, we need to make sure an array is returned at the end.

trial = {
  type: jsPsychHtmlButtonResponse,
  stimulus: `<p style="font-size:48px; color:red;">GREEN</p>`,
  choices: function(){
    // start with the set of response options
    let options = ['Red', 'Green', 'Blue', 'Yellow'];
    
    // randomize the order of the array
    options = jsPsych.randomization.shuffle(options);
    
    // return the full array
    return options
  },
  prompt: "<p>What color is the ink?</p>",
  save_trial_parameters: {
    choices: true
  }
};

Here’ a full example with randomized button orders:

 <!DOCTYPE html>
<html>
<head>
    <title>Simon Task</title>
    <!-- jsPsych -->
    <script src="jspsych/jspsych.js"></script>
    <link href="jspsych/jspsych.css" rel="stylesheet" type="text/css" />
    
    <!-- jPsych plugins -->
    <script src="jspsych/plugin-html-keyboard-response.js"></script>

    <!-- custom CSS -->
     <link href="style.css" rel="stylesheet" type="text/css" />
</head>
<body>
  <!-- custom JS -->
  <script src="exp.js"></script>
</body>
</html>
 
 // 1. Initialize jsPsych
const jsPsych = initJsPsych();

 
// 2. Define our trials
const welcome = {
  type: jsPsychHtmlKeyboardResponse,
  stimulus: `<p>Welcome to the Experiment!</p>
              <p>Press any key to begin.</p>`,
  choices: "ALL_KEYS", 
  post_trial_gap: 250
}

let stroop ={
  timeline: [
      {
        type: jsPsychHtmlButtonResponse,
        stimulus: function(){
            return  `<p style="font-size:48px; color:${jsPsych.evaluateTimelineVariable("color")};">${jsPsych.evaluateTimelineVariable("word")}</p>`
        },
        choices: function(){
          console.log(this.data)
          // start with the set of response options
          let options = ["Red", "Green", "Blue", "Yellow"];
          
          // randomize the order of the array
          options = jsPsych.randomization.shuffle(options);
          
          // return the full array
          return options
        },
        prompt: "<p>What color is the ink?</p>",
        post_trial_gap: 250
      }
  ],
  timeline_variables: [
    {word: "RED", color: "red"},
    {word: "BLUE", color: "blue"},
    {word: "GREEN", color: "yellow"},
    {word: "RED", color: "green"}
  ],
  randomize_order: true,
  save_trial_parameters: {
    choices: true
  }
}

// 3. Run jsPsych with our trials
jsPsych.run([
  welcome,
  stroop
]); 
  
Live JsPsych Demo Click inside the demo to activate demo

We could also make it dependent on how they responded on a previous trial:

17.7 Prompt

We can also update the prompt that appears below each stimulus in a dynamic way. Let’s try providing performance feedback below the stimulus, so they know how they’ve been performing on average.

This one is a bit complicated, so first, here’s the prompt function alone. We’re chaining together filter methods to restrict which trials are retrieved and perform some calculations on the RTs and accuracy rates.

Then, I’ll use if-then logic to change what is returned.

prompt: function() {
        // Get the last 5 trials (excluding practice/instruction trials)
        let recentTrials = jsPsych.data.get()
            .filter({task: 'stroop'})
            .last(5);

        // Wait until there is at least 5 trials before giving feedback
        if (recentTrials.count() < 5) {
            // if you call return early none of the code below will run!
            return "<p>Press R for red, G for green, B for blue, Y for yellow</p>";
        }

        // Calculate average RT and accuracy
        let avgRT = Math.round(recentTrials.select('rt').mean());
        let accuracy = (recentTrials.filter({correct: true}).count() / 5) * 100;

        // Determine feedback message
        let feedback = "";
        if (accuracy < 80) {
            feedback = "[Too many errors - focus on accuracy!]";
        } else if (avgRT > 1000) {
            feedback = "[Too slow - try to respond faster!]";
        } else if (accuracy >= 90 && avgRT <= 700) {
            feedback = "[Great job - fast and accurate!]";
        } else if (accuracy >= 85) {
            feedback = "[Good accuracy!]";
        } else {
            feedback = "[Keep it up!]";
        }

        return `<p>Last 5 trials: ${avgRT}ms average, ${accuracy.toFixed(0)}% correct ${feedback}</p>
                <p>Press R for red, G for green, B for blue, Y for yellow</p>`;
    }

Let’s see how that looks in a full example:

 <!DOCTYPE html>
<html>
<head>
    <title>Simon Task</title>
    <!-- jsPsych -->
    <script src="jspsych/jspsych.js"></script>
    <link href="jspsych/jspsych.css" rel="stylesheet" type="text/css" />
    
    <!-- jPsych plugins -->
    <script src="jspsych/plugin-html-keyboard-response.js"></script>

    <!-- custom CSS -->
     <link href="style.css" rel="stylesheet" type="text/css" />
</head>
<body>
  <!-- custom JS -->
  <script src="exp.js"></script>
</body>
</html>
 
 // 1. Initialize jsPsych
const jsPsych = initJsPsych();

 
// 2. Define our trials
const welcome = {
  type: jsPsychHtmlKeyboardResponse,
  stimulus: `<p>Welcome to the Experiment!</p>
              <p>Press any key to begin.</p>`,
  choices: "ALL_KEYS", 
  post_trial_gap: 250
}

let stroop ={
  timeline: [
      {
          type: jsPsychHtmlKeyboardResponse,
          stimulus: function(){
                        let output = `<p style="font-size: 48px; color: ${jsPsych.evaluateTimelineVariable("color")};">
                                        ${jsPsych.evaluateTimelineVariable("word")}
                                      </p>`
                        return output
          },
          prompt: function() {
              // Get the last 5 trials (excluding practice/instruction trials)
              let recentTrials = jsPsych.data.get()
                  .filter({task: "stroop"})
                  .last(5);
      
              if (recentTrials.count() < 5) {
                  return "<p>Press R for red, G for green, B for blue, Y for yellow</p>";
              }
      
              // Calculate average RT and accuracy
              let avgRT = Math.round(recentTrials.select("rt").mean());
              let accuracy = (recentTrials.filter({correct: true}).count() / 5) * 100;
      
              // Determine feedback message
              let feedback = "";
              if (accuracy < 80) {
                  feedback = "[Too many errors - focus on accuracy!]";
              } else if (avgRT > 1000) {
                  feedback = "[Too slow - try to respond faster!]";
              } else if (accuracy >= 90 && avgRT <= 700) {
                  feedback = "[Great job - fast and accurate!]";
              } else if (accuracy >= 85) {
                  feedback = "[Good accuracy!]";
              } else {
                  feedback = "[Keep it up!]";
              }
      
              return `<p>Last 5 trials: ${avgRT}ms average, ${accuracy.toFixed(0)}% correct ${feedback}</p>
                      <p>Press R for red, G for green, B for blue, Y for yellow</p>`;
          },
          choices: ["r", "g", "b", "y"],
          post_trial_gap: 500,
          data: {
              task: "stroop",
              word: jsPsych.timelineVariable("word"),
              color: jsPsych.timelineVariable("color")
          },
          on_finish: function(data) {
              let colorKeyMap = {
                  "r": "red",
                  "g": "green", 
                  "b": "blue",
                  "y": "yellow"
              };
              data.correct = colorKeyMap[data.response] === data.color;
          }
      }
  ],
  timeline_variables: [
    {word: "RED", color: "red"},
    {word: "RED", color: "blue"},
    {word: "RED", color: "green"},
    {word: "RED", color: "yellow"},

    {word: "BLUE", color: "red"},
    {word: "BLUE", color: "blue"},
    {word: "BLUE", color: "green"},
    {word: "BLUE", color: "yellow"},

    {word: "GREEN", color: "red"},
    {word: "GREEN", color: "blue"},
    {word: "GREEN", color: "green"},
    {word: "GREEN", color: "yellow"},

    {word: "YELLOW", color: "red"},
    {word: "YELLOW", color: "blue"},
    {word: "YELLOW", color: "green"},
    {word: "YELLOW", color: "yellow"},
  ],
  randomize_order: true,
  repetitions: 4
}

// 3. Run jsPsych with our trials
jsPsych.run([
  welcome,
  stroop
]); 
  
Live JsPsych Demo Click inside the demo to activate demo

17.8 Summary

Dynamic parameters offer significant advantages over static values. By replacing parameters with functions, you can build adaptive experiments that provide personalized feedback, adjust difficulty based on performance, and create complex HTML displays that update with timeline variables. This would not be possible with pre-defined timeline variables alone.

The key insight is straightforward: any jsPsych parameter can become dynamic by replacing it with a function that returns the appropriate format. This transforms your static experiment into one that responds to individual participant behaviour. Keep three important points in mind if you’re adding dynamic parameters: use jsPsych.evaluateTimelineVariable() within functions, save any dynamic parameters you want to analyze, and ensure your functions return values in the format jsPsych expects.