9  Timelines

learning goals
  • Understand the limitations of individual trial objects and why timelines are important for complex experiments
  • Create nested timelines to define common parameters once and apply them to multiple trials
  • Use timeline variables to separate experimental procedures from stimulus-specific values
  • Implement randomization and repetition using timeline parameters
  • Structure experiments hierarchically with modular timeline components
  • Apply best practices for timeline design including meaningful variable names and useful metadata
  • Build efficient, maintainable experiment code that scales from simple to complex designs

9.1 Introduction

In the previous chapter, you learned to create individual trials by defining trial objects with specific parameters. While this approach works well for simple experiments, real psychological studies typically involve many trials that follow similar patterns with slight variations. Imagine having to manually code 100 individual trials for a lexical decision experiment where each trial requires separate lines of code. This approach quickly becomes unwieldy and error-prone. Another problem with manually coding each trial is that they always appear in the same order. We typically want some randomization to control for order effects. We can’t easily do this when we manually code each trial.

This is where timelines become important A timeline in jsPsych is the backbone of your experiment. It defines the structure and sequence of all trials. Think of it as the experimental protocol that organizes how your study unfolds from start to finish.

9.2 Why Use Timelines?

9.2.1 The Problem with Individual Trial Objects

Consider the lexical decision task from the previous chapter, where participants see a string of letters and decide whether it forms a real word or not. A typical experiment might test 50 words and 50 nonwords. Using the individual trial approach, you would need to create 100 separate trial objects:

// Word trials
const trial_1 = {
  type: jsPsychHtmlKeyboardResponse,
  stimulus: '<div style="font-size: 48px;">HOUSE</div>',
  choices: ['f', 'j'],
  prompt: '<p>Press F for word, J for nonword</p>'
};

const trial_2 = {
  type: jsPsychHtmlKeyboardResponse,
  stimulus: '<div style="font-size: 48px;">TABLE</div>',
  choices: ['f', 'j'],
  prompt: '<p>Press F for word, J for nonword</p>'
};

// Nonword trials
const trial_3 = {
  type: jsPsychHtmlKeyboardResponse,
  stimulus: '<div style="font-size: 48px;">BLAFE</div>',
  choices: ['f', 'j'],
  prompt: '<p>Press F for word, J for nonword</p>'
};

const trial_4 = {
  type: jsPsychHtmlKeyboardResponse,
  stimulus: '<div style="font-size: 48px;">GLINT</div>',
  choices: ['f', 'j'],
  prompt: '<p>Press F for word, J for nonword</p>'
};


jsPsych.run([
 trial_1,
 trial_2,
 trial_3,
 trial_4
]);

This approach creates several problems. First, you’re writing essentially the same code over and over, which is repetitive and time-consuming. Second, it’s error-prone because it’s easy to make typos or forget to change parameters. Third, if you want to modify something like the font size or the response keys, you need to edit 100 different objects. Finally, the order of the trials is hard-coded, which means everyone sees the same order of trials every time.

9.2.2 The Timeline Solution

Timelines solve these problems by allowing you to define procedures once and apply them to multiple stimuli, organize trials hierarchically with nested structures, easily modify parameters that apply to multiple trials, and implement randomization efficiently.

9.3 Basic Timeline Structure

9.3.1 A Single Trial Timeline

Let’s start by reviewing the way we’ve already been programming our experiments, but discuss it in terms of being a timeline. The simplest timeline contains just one trial. Instead of running a trial directly, you embed it in an array. This should look familiar:

const trial = {
  type: jsPsychHtmlKeyboardResponse,
  stimulus: 'Welcome to the experiment.'
};

const timeline = [trial];
jsPsych.run(timeline);

9.3.2 Multiple Trials Timeline

For multiple trials, simply add more trial objects to the timeline array. Again, this should look fairly familiar:

const timeline = [];

const welcome_trial = {
  type: jsPsychHtmlKeyboardResponse,
  stimulus: 'Welcome to the lexical decision experiment. Press any key to continue.'
};

const instruction_trial = {
  type: jsPsychHtmlKeyboardResponse,
  stimulus: 'You will see letter strings. Press F if it is a real word, J if it is not a real word. Press any key to begin.'
};

const ready_trial = {
  type: jsPsychHtmlKeyboardResponse,
  stimulus: 'Ready? Press SPACE to start.',
  choices: [' ']
};

let timeline = [
 welcome_trial,
 instruction_trial,
 ready_trial
]

jsPsych.run(timeline);

This is a timeline because jsPsych will run this in the order you input them in the array. If I were to change the order of this timeline, it would change the order of the events.

let timeline = [
 ready_trial,   // first
 welcome_trial   // second
 instruction_trial // third
]

9.4 Nested Timelines

Now, let’s expand on this basic principle to create more complex experimental procedures. One of the most powerful features of timelines is the ability to nest them. This allows you to define common parameters once and apply them to multiple trials. Again, think of our lexical decision task. Everything about each trial is identical other than the word stimulus itself. The nested timeline will allow us to write all of that code once, and indicate that the word should change each time.

9.4.1 Basic Nested Timeline

Instead of creating individual trial objects, we can define the procedure once:

const jsPsych = initJsPsych();

const welcome_trial = {
  type: jsPsychHtmlKeyboardResponse,
  stimulus: 'Welcome to the. Press any key to continue.'
};

const lexical_decision = {
  type: jsPsychHtmlKeyboardResponse,
  choices: ['f', 'j'],
  prompt: '<p>Press F for word, J for nonword</p>',
  timeline: [
    {stimulus: 'HOUSE'},
    {stimulus: 'TABLE'},
    {stimulus: 'BLAFE'},
    {stimulus: 'GLINT'}
  ]
};

const timeline = [welcome_trial, lexical_decision]
jsPsych.run(timeline)
Live JsPsych Demo Click inside the demo to activate demo

This creates four trials that all share the same type, choices, and prompt parameters, but each displays a different letter string. When we write a nested timeline like this we can put parameters in the trial object, which then are applied to all the trials in the timeline. In this example, type, choices and prompt are applied to all four trials in the timeline. We can also put parameters inside each individual trial inside the timeline, which will just apply to that trial. In this case, we’re changing the stimulus on each trial. At the moment, the timeline still runs in the order they’re written: HOUSE –> TABLE –> BLAFE –>GLINT .

9.4.2 Overriding Parameters

You can override shared parameters for specific trials when needed:

const jsPsych = initJsPsych();

const welcome_trial = {
  type: jsPsychHtmlKeyboardResponse,
  stimulus: 'Welcome to the. Press any key to continue.'
};

const lexical_decision = {
  type: jsPsychHtmlKeyboardResponse,
  choices: ['f', 'j'],
  prompt: '<p>Press F for word, J for nonword</p>',
  timeline: [
    {stimulus: 'HOUSE'},
    {stimulus: 'TABLE'},
    {
     stimulus: 'BLAFE',
     prompt: "<p>This is an attention check: Press F.</p>"
    },
    {stimulus: 'GLINT'}
  ]
};

const timeline = [welcome_trial, lexical_decision]
jsPsych.run(timeline)
Live JsPsych Demo Click inside the demo to activate demo

The third trial will display the custom prompt instead of the default one.

9.5 Timeline Variables

The nested timeline approach we just learned is a good first step, but we’re still not being as efficient as we could be. If we wanted to change the font size or color, we’d need to edit every single stimulus line. If we wanted to add a fixation before each trial, we’d have to write it out each time. More importantly, we have no easy way to randomize the order of these trials or to add additional information about each stimulus that might be useful for our analysis.

Timeline variables solve these problems by separating the experimental procedure from the specific values that change from trial to trial. Instead of hardcoding each variation, we define what varies and let jsPsych handle the repetition for us.

9.5.1 Understanding Timeline Variables

Timeline variables work on a simple principle: you define your experimental procedure once, then specify a list of variables and their values for each trial. jsPsych automatically runs through your procedure multiple times, using different variable values each time.

Think of it like a mail merge in a word processor. You write a letter template once (“Dear [NAME], thank you for your [DONATION_AMOUNT] donation”), then provide a list of names and donation amounts. The software creates personalized letters by filling in the variables. Timeline variables work the same way for experiments.

9.5.2 Basic Timeline Variables Examples

Let’s start by rewriting our lexical decision task using timeline variables. We’ll create the same lexical_decision object with a timeline, except instead of writing each trial inside the timeline, I’ll just write one trial and for the stimulus, I’ll put in a placeholder function called jsPsych.timelineVariable(). Then, I create a new array called timeline_variables. This array contains objects with key-value pairs. These can be anything I want, and in this case, I make the key word and the values will be the words that change on each trial.

const jsPsych = initJsPsych();

const welcome_trial = {
  type: jsPsychHtmlKeyboardResponse,
  stimulus: 'Welcome to the. Press any key to continue.'
};

const lexical_decision = {
  timeline: [
    {
      type: jsPsychHtmlKeyboardResponse,
      stimulus: jsPsych.timelineVariable('word'),
      choices: ['f', 'j'],
      prompt: '<p>Press F for word, J for nonword</p>'
    }
  ],
  timeline_variables: [
    { word: 'HOUSE' },
    { word: 'TABLE' },
    { word: 'BLAFE' },
    { word: 'GLINT' }
  ]
};

const timeline = [welcome_trial, lexical_decision]
jsPsych.run(timeline)
Live JsPsych Demo Click inside the demo to activate demo

This creates a complete procedure that will run four times, once for each item in the timeline_variables array.

jsPsych.timelineVariable('word'): Use this to reference timeline variables in trial parameters. It creates a placeholder that jsPsych evaluates at the right time. This placeholder simply tells jsPsych to take whatever is in word and place it in the stimulus on that trial.

You can combine multiple timeline variables and replace any of the trial parameters that we want to change on that trial.

const jsPsych = initJsPsych();

const welcome_trial = {
  type: jsPsychHtmlKeyboardResponse,
  stimulus: 'Welcome to the. Press any key to continue.'
};

const lexical_decision = {
  type: jsPsychHtmlKeyboardResponse,
  choices: ['f', 'j'],
  timeline: [
    {
      type: jsPsychHtmlKeyboardResponse,
      stimulus: jsPsych.timelineVariable('word'),
      choices: jsPsych.timelineVariable('myChoices'),
      prompt: jsPsych.timelineVariable('myPrompt')
    }
  ],
  timeline_variables: [
    {
     word: 'HOUSE', 
     myPrompt: '<p>Press F for word, J for nonword</p>', 
     myChoices: ['f', 'j']
    },
    {
     word: 'TABLE', 
     myPrompt: '<p>Press F for word, J for nonword</p>', 
     myChoices: ['f', 'j']
    },
    {
     word: 'BLAFE', 
     myPrompt: '<p>This is an attention check: Press X.</p>', 
     myChoices: ['f', 'j', 'x']
    },
    {
     word: 'GLINT', 
     myPrompt: '<p>Press F for word, J for nonword</p>', 
     myChoices: ['f', 'j']
    }
  ]
};

const timeline = [welcome_trial, lexical_decision]
jsPsych.run(timeline)
Live JsPsych Demo Click inside the demo to activate demo

Notice how I can replace any of my trial parameters and define it for that particular trial inside my timeline_variables. Also notice that I still have to follow the format required for that parameter: stimulus and prompt take text or HTML and choices requires an array.

9.5.3 Adding More Events

One of the benefits of defining our timeline this way is that we can add more events to our timeline loop. For instance, we often put a ‘fixation’ cross before our stimulus. The fixation cross shows the participant where they should pay attention and get ready for the trial. To add a fixation to every trial, we just need to insert it into our timline loop:

const jsPsych = initJsPsych();

const welcome_trial = {
  type: jsPsychHtmlKeyboardResponse,
  stimulus: 'Welcome to the. Press any key to continue.'
};

const lexical_decision = {
  type: jsPsychHtmlKeyboardResponse,
  choices: ['f', 'j'],
  timeline: [
    {
     type: jsPsychHtmlKeyboardResponse,
     stimulus: `<span style="font-size: 48px"> + </span>`,
     choices: 'NO_KEYS',
     trial_duration: 1000
    },
    {
     type: jsPsychHtmlKeyboardResponse,
     stimulus: jsPsych.timelineVariable('word'),
     choices: jsPsych.timelineVariable('myChoices'),
     prompt: jsPsych.timelineVariable('myPrompt')
    }
  ],
  timeline_variables: [
    {
     word: 'HOUSE', 
     myPrompt: '<p>Press F for word, J for nonword</p>', 
     myChoices: ['f', 'j']
    },
    {
     word: 'TABLE', 
     myPrompt: '<p>Press F for word, J for nonword</p>', 
     myChoices: ['f', 'j']
    },
    {
     word: 'BLAFE', 
     myPrompt: '<p>This is an attention check: Press X.</p>', 
     myChoices: ['f', 'j', 'x']
    },
    {
     word: 'GLINT', 
     myPrompt: '<p>Press F for word, J for nonword</p>', 
     myChoices: ['f', 'j']
    }
  ]
};

const timeline = [welcome_trial, lexical_decision]
jsPsych.run(timeline)
Live JsPsych Demo Click inside the demo to activate demo

The timeline always goes in order fixation –> word, then loops and repeats to go through each of our timeline_variables. We can add as many more events as we’d like:

const jsPsych = initJsPsych();

const welcome_trial = {
  type: jsPsychHtmlKeyboardResponse,
  stimulus: 'Welcome to the. Press any key to continue.'
};

const lexical_decision = {
  type: jsPsychHtmlKeyboardResponse,
  choices: ['f', 'j'],
  timeline: [
    {
     type: jsPsychHtmlKeyboardResponse,
     stimulus: `<span style="font-size: 48px"> + </span>`,
     choices: 'NO_KEYS',
     trial_duration: 1000
    },
    {
     type: jsPsychHtmlKeyboardResponse,
     stimulus: jsPsych.timelineVariable('word'),
     choices: jsPsych.timelineVariable('myChoices'),
     prompt: jsPsych.timelineVariable('myPrompt')
    },
    {
     type: jsPsychHtmlKeyboardResponse,
     stimulus: `<span style='font-size: 48px; color: Tomato'> Next Trial in... 3 </span>`,
     choices: 'NO_KEYS',
     trial_duration: 500
    },
    {
     type: jsPsychHtmlKeyboardResponse,
     stimulus: `<span style='font-size: 48px; color: Gold'> Next Trial in... 2 </span>`,
     choices: 'NO_KEYS',
     trial_duration: 500
    },
    {
     type: jsPsychHtmlKeyboardResponse,
     stimulus: `<span style='font-size: 48px; color: MediumSeaGreen'> Next Trial in... 1 </span>`,
     choices: 'NO_KEYS',
     trial_duration: 500
    }
  ],
  timeline_variables: [
    {
     word: 'HOUSE', 
     myPrompt: '<p>Press F for word, J for nonword</p>', 
     myChoices: ['f', 'j']
    },
    {
     word: 'TABLE', 
     myPrompt: '<p>Press F for word, J for nonword</p>', 
     myChoices: ['f', 'j']
    },
    {
     word: 'BLAFE', 
     myPrompt: '<p>This is an attention check: Press X.</p>', 
     myChoices: ['f', 'j', 'x']
    },
    {
     word: 'GLINT', 
     myPrompt: '<p>Press F for word, J for nonword</p>', 
     myChoices: ['f', 'j']
    }
  ]
};

const timeline = [welcome_trial, lexical_decision]
jsPsych.run(timeline)
Live JsPsych Demo Click inside the demo to activate demo

An important note to remember is that each event in our timeline has access to our timeline_variables. This means we can define multiple parts of our experiment and change them on a trial-to-trial basis.

For example, in some experiments we present a ‘cue’ then a target to see what effect the cue might have on performance. Both the cue and the target need to change each trial. We can do that fairly easily using our timeline_variables. In this example, you see an arrow cue (e.g., ) followed by a word LEFT or RIGHT. The task is to ignore the arrow and indicate whether the word is LEFT or RIGHT by pressing “Z” for left and “M” for right.

const jsPsych = initJsPsych();

const welcome_trial = {
  type: jsPsychHtmlKeyboardResponse,
  stimulus: 'Welcome to the. Press any key to continue.'
};

const attention_task = {
  timeline: [
    {
     type: jsPsychHtmlKeyboardResponse,
     stimulus: `+`,
     prompt: `<p>Press 'Z' if the word is LEFT. Press 'M' if the word is RIGHT</p>`,
     choices: 'NO_KEYS',
     trial_duration: 1000
    },
    {
      type: jsPsychHtmlKeyboardResponse,
      stimulus: jsPsych.timelineVariable('cue'),
      prompt: `<p>Press 'Z' if the word is LEFT. Press 'M' if the word is RIGHT</p>`,
      choices: "NO_KEYS",
      trial_duration: 200
    },
    {
      type: jsPsychHtmlKeyboardResponse,
      stimulus: jsPsych.timelineVariable('target'),
      prompt: `<p>Press 'Z' if the word is LEFT. Press 'M' if the word is RIGHT</p>`,
      choices: ['z', 'm']
    }
  ],
  timeline_variables: [
    { cue: '→', target: 'LEFT' },
    { cue: '→', target: 'RIGHT' },
    { cue: '←', target: 'LEFT' },
    { cue: '←', target: 'RIGHT' }
  ]
};

const timeline = [welcome_trial, attention_task]
jsPsych.run(timeline)
Live JsPsych Demo Click inside the demo to activate demo

9.6 Quick Randomization and Repetition

Another benefit to using a list of timeline_variables is that we can quickly add some randomization to our experiment. One of the parameters available to us is called randomize_order, which we can set to true or false. By setting it to true jsPsych will go through our four trials, but in a random order.

We can also tell jsPsych how many times we want it to go through our timeline_variables by setting repetitions.

Just keep in mind that the randomization occurs within each repetition. That is, if I set it to repeat twice, it will present all four trials in a random order, then repeat.

const jsPsych = initJsPsych();

const welcome_trial = {
  type: jsPsychHtmlKeyboardResponse,
  stimulus: 'Welcome to the. Press any key to continue.'
};

const attention_task = {
  timeline: [
    {
     type: jsPsychHtmlKeyboardResponse,
     stimulus: `+`,
     prompt: `<p>Press 'Z' if the word is LEFT. Press 'M' if the word is RIGHT</p>`,
     choices: 'NO_KEYS',
     trial_duration: 1000
    },
    {
      type: jsPsychHtmlKeyboardResponse,
      stimulus: jsPsych.timelineVariable('cue'),
      prompt: `<p>Press 'Z' if the word is LEFT. Press 'M' if the word is RIGHT</p>`,
      choices: "NO_KEYS",
      trial_duration: 200
    },
    {
      type: jsPsychHtmlKeyboardResponse,
      stimulus: jsPsych.timelineVariable('target'),
      prompt: `<p>Press 'Z' if the word is LEFT. Press 'M' if the word is RIGHT</p>`,
      choices: ['z', 'm']
    }
  ],
  timeline_variables: [
    { cue: '→', target: 'LEFT' },
    { cue: '→', target: 'RIGHT' },
    { cue: '←', target: 'LEFT' },
    { cue: '←', target: 'RIGHT' }
  ],
  randomize_order: true,
  repetitions: 2
};

const timeline = [welcome_trial, attention_task]
jsPsych.run(timeline)
Live JsPsych Demo Click inside the demo to activate demo

In a later Unit we’ll discuss more complex methods for randomization. For now, simply randomizing the order of our stimuli is good enough for most situations.

9.7 Best Practices for Timeline Design

9.7.1 Plan Your Experimental Structure First

Before coding, sketch out your experimental procedure. What is the basic trial structure? What parameters vary between trials? What parameters stay constant? How many repetitions do you need?

9.7.2 Use Meaningful Variable Names

Choose descriptive names for your timeline variables:

// Good
timeline_variables: [
  { target_word: 'cat', distractor_word: 'dog', condition: 'related' },
  { target_word: 'car', distractor_word: 'pen', condition: 'unrelated' }
]

// Less clear
timeline_variables: [
  { stim1: 'cat', stim2: 'dog', cond: 'rel' },
  { stim1: 'car', stim2: 'pen', cond: 'unrel' }
]

9.7.3 Include Useful Information in Timeline Variables

Add information that will help you understand your experiment structure:

timeline_variables: [
  { 
    stimulus: 'HOUSE', 
    item_type: 'word',
    frequency: 'high',
    length: 5
  },
  { 
    stimulus: 'BLAFE', 
    item_type: 'nonword',
    frequency: 'na',
    length: 5
  }
]

9.7.4 Keep Procedures Modular

Break complex experiments into separate timeline procedures:

const instructions_timeline = [...];
const practice_timeline = [...];
const main_experiment_timeline = [...];
const debriefing_timeline = [...];

const full_experiment = [
  instructions_timeline,
  practice_timeline, 
  main_experiment_timeline,
  debriefing_timeline
];

9.8 Summary

Timelines are the foundation of well-structured jsPsych experiments. They allow you to organize trials hierarchically using nested timelines, eliminate repetitive code by defining procedures once, create flexible experiments using timeline variables, and maintain clean, readable code that’s easy to modify.

Timeline variables are particularly powerful for psychological research because they match how we typically think about experiments: repeating the same procedure with different stimuli or conditions. Instead of creating dozens of individual trial objects, you define the experimental procedure once and specify the varying parameters in the timeline_variables array.