A lead researcher explaining the benefits of Cognition.run

Developing a Stroop Effect Experiment with jsPsych

The Stroop effect in brief

In 1935 John Ridley Stroop published Studies of interference in serial verbal reactions in the Journal of Experimental Psychology. He showed that naming the ink colour of a colour word is markedly slower when the word and the ink conflict (the word RED printed in blue) than when a neutral stimulus is used, whereas reading the word itself is barely affected by the ink colour. The slowing on incongruent trials, relative to congruent or neutral trials, is the Stroop effect.

The task is a standard measure of cognitive interference and inhibitory control: reading is highly automatic and must be suppressed in favour of the less practised colour-naming response. The usual dependent measures are reaction time and error rate on congruent versus incongruent trials, and the difference between them is the interference score. Computerised versions replace vocal naming with key presses, as in the tutorial below.

If you want to see a finished version first, try our online Stroop test demo.

Step-by-step guide

The code uses jsPsych 7/8 syntax and the jsPsychHtmlKeyboardResponse and jsPsychInstructions plugins.

  1. Initialize jsPsych
    const jsPsych = initJsPsych({
        on_finish: function() {
            jsPsych.data.displayData('csv');
        }
    });

    This creates the jsPsych instance and specifies that the collected data will be displayed in CSV format when the experiment ends.

  2. Define experiment parameters
    const colours = ['red', 'green', 'blue', 'yellow'];
    const n_trials = 15;

    The colour names double as the words shown on screen. n_trials sets how many Stroop trials are run; 15 is enough for a demo, while a real study would use considerably more.

  3. Create functions for congruent and incongruent conditions
    function congruent() {
        const colour_list = jsPsych.randomization.sampleWithReplacement(colours, 1);
        return { text: colour_list[0], colour: colour_list[0], condition: 'congruent' };
    }
    
    function incongruent() {
        const colour_list = jsPsych.randomization.sampleWithoutReplacement(colours, 2);
        return { text: colour_list[0], colour: colour_list[1], condition: 'incongruent' };
    }

    Each function returns an object describing one stimulus. In the congruent case the word and the ink colour are the same; in the incongruent case two different colours are sampled without replacement, so word and ink always differ.

  4. Set up the instructions
    const instructions = {
        type: jsPsychInstructions,
        pages: [
          "Welcome to the experiment.<br>Press Space to continue.",
          "In this experiment you will be presented with the words blue, red, yellow and green.<br>Press Space to continue.",
          "As soon as you see a new word, press its first letter.<br>For example, press the B key for blue.<br>Press Space to continue.",
          "Try to answer as quickly as you can!<br>Press Space to start the experiment.",
        ],
        key_forward: ' '
    };

    The jsPsychInstructions plugin shows a sequence of pages that the participant advances with the space bar. Note that the instructions here ask for the first letter of the word; to measure interference in the classic direction you would instead ask for the first letter of the ink colour. Change the wording to match whichever version you intend to run.

  5. Design the trial structure
    const fixation = {
        type: jsPsychHtmlKeyboardResponse,
        stimulus: '<p style="font-size:60px">+</p>',
        trial_duration: 500,
        response_ends_trial: false
    };
    
    const iti = {
        type: jsPsychHtmlKeyboardResponse,
        stimulus: '',
        trial_duration: 250,
        response_ends_trial: false
    };

    The fixation cross is shown for 500 ms before each stimulus, and the inter-trial interval (ITI) is a 250 ms blank screen. Both ignore key presses.

  6. Construct the experiment timeline
    const trials = [instructions];
    for (let i = 0; i < n_trials; i++) {
        let values;
        if (Math.random() < 0.5) {
            values = congruent();
        } else {
            values = incongruent();
        }
        const trial = {
            type: jsPsychHtmlKeyboardResponse,
            stimulus: '<p style="font-size:60px;color: ' + values.colour + '">' + values.text + '</p>',
            choices: ['r', 'g', 'b', 'y'],
            data: values
        };
        trials.push(iti);
        trials.push(fixation);
        trials.push(trial);
    }

    Each iteration picks a congruent or incongruent stimulus at random and pushes the ITI, the fixation and the stimulus trial onto the timeline. Passing values as data stores the word, the ink colour and the condition alongside the response and reaction time, which is what you need to compute the interference score afterwards.

  7. Run the experiment
    jsPsych.run(trials);

    This starts the experiment with the timeline you built.

Complete Stroop effect experiment code

const jsPsych = initJsPsych({
    on_finish: function() {
        jsPsych.data.displayData('csv');
    }
});

// the colours are also the words
const colours = ['red', 'green', 'blue', 'yellow'];

const n_trials = 15;

// returns an object { text, colour, condition } where text and colour match
function congruent() {
    // when we're only picking one, with/without replacement doesn't matter
    const colour_list = jsPsych.randomization.sampleWithReplacement(colours, 1);
    return { text: colour_list[0], colour: colour_list[0], condition: 'congruent' };
}

// returns an object { text, colour, condition } where text and colour differ
function incongruent() {
    // pick two colours without replacement (i.e. they will be different)
    const colour_list = jsPsych.randomization.sampleWithoutReplacement(colours, 2);
    return { text: colour_list[0], colour: colour_list[1], condition: 'incongruent' };
}

// these are in HTML, so <br> means "line break"
const instructions = {
    type: jsPsychInstructions,
    pages: [
      "Welcome to the experiment.<br>Press Space to continue.",
      "In this experiment you will be presented with the words blue, red, yellow and green.<br>Press Space to continue.",
      "As soon as you see a new word, press its first letter.<br>For example, press the B key for blue.<br>Press Space to continue.",
      "Try to answer as quickly as you can!<br>Press Space to start the experiment.",
    ],
    key_forward: ' '
};

const fixation = {
    type: jsPsychHtmlKeyboardResponse,
    stimulus: '<p style="font-size:60px">+</p>',
    trial_duration: 500,
    response_ends_trial: false
};

// blank (ITI stands for "inter trial interval")
const iti = {
    type: jsPsychHtmlKeyboardResponse,
    stimulus: '',
    trial_duration: 250,
    response_ends_trial: false
};

const trials = [instructions];
// repeat this code n_trials times
for (let i = 0; i < n_trials; i++) {
    let values;
    // Math.random returns a number between 0 and 1. Use it to decide
    // whether the current trial is congruent or incongruent.
    if (Math.random() < 0.5) {
        values = congruent();
    } else {
        values = incongruent();
    }
    const trial = {
        type: jsPsychHtmlKeyboardResponse,
        stimulus: '<p style="font-size:60px;color: ' + values.colour + '">' + values.text + '</p>',
        // 'choices' restricts the available responses for the participant
        choices: ['r', 'g', 'b', 'y'],
        data: values
    };
    trials.push(iti);
    trials.push(fixation);
    trials.push(trial);
}

jsPsych.run(trials);

With this code you have a functional Stroop experiment. Natural extensions are adding an on_finish to the stimulus trial that scores data.correct with jsPsych.pluginAPI.compareKeys, adding neutral trials (for example, a row of Xs in colour), and balancing the number of congruent and incongruent trials instead of drawing them at random.

Running it online with Cognition

To collect data from real participants, paste the complete code into the Cognition editor, select jsPsych 8 (or 7) as the library version, and check the live preview; the built-in console will flag any syntax error before you share the task. Saving gives you a participant link of the form https://<token>.cognition.run.

You do not need to write any code to save the data. Cognition uploads each trial to the server as soon as it finishes, buffers trials locally through a service worker if the participant's connection drops, and flushes everything before your on_finish callback runs. That means you can replace jsPsych.data.displayData('csv'), which is only useful while developing, with whatever should happen at the end of the session, such as a thank-you screen or a redirect to a Prolific completion URL. Our article on integrating jsPsych with Prolific, SONA and MTurk explains how to read the participant ID from the URL (it is added automatically as a column to every trial) and how to redirect safely.

Afterwards, download the data as CSV or JSON, one file per participant or a single file, with the condition, text, colour, response and rt columns ready for analysis. Create a free account to host up to 4 tasks with 60 participants each, or see the documentation for consent forms, single-use links and stopping recruitment after a set number of participants.