jsPsych Integration
This tutorial walks through adding Chiasm eye-tracking to an existing jsPsych experiment, one step at a time. By the end you will have a working experiment that calibrates the participant, records gaze data during each trial, and cleans up afterwards.
The integration uses three packages from jsPsych contrib:
@jspsych-contrib/extension-chiasmstarts and stops a recording for each trial you attach it to.@jspsych-contrib/plugin-chiasm-setupinitializes the tracker and opens a session.@jspsych-contrib/plugin-chiasm-calibrateruns the participant-facing calibration ceremony.
Prerequisites
Before you begin, make sure you have:
- A working jsPsych experiment (v8+) served over
https://orhttp://localhost. - Created an Experiment in the Chiasm dashboard.
- Copied your auth token and experiment ID from the dashboard — you will need both in the code below.
Starting Point
Below is a simple jsPsych experiment that shows a welcome screen, then two images. The participant presses "y" or "n" for each image. There is no eye-tracking yet — this is the code we will be modifying.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>jsPsych Demo</title>
<!-- jsPsych Core -->
<script src="https://unpkg.com/jspsych@8.2.2"></script>
<link href="https://unpkg.com/jspsych@8.2.2/css/jspsych.css" rel="stylesheet" type="text/css" />
<!-- jsPsych Plugins -->
<script src="https://unpkg.com/@jspsych/plugin-image-keyboard-response@2.1.0"></script>
<script src="https://unpkg.com/@jspsych/plugin-preload@2.1.0"></script>
<script src="https://unpkg.com/@jspsych/plugin-instructions@2.1.0"></script>
</head>
<body></body>
<script>
const jsPsych = initJsPsych({
default_iti: 250,
on_finish: function () {
jsPsych.data.displayData();
}
});
const preload = { type: jsPsychPreload, auto_preload: true };
const welcome = {
type: jsPsychInstructions,
pages: ["<p>Welcome. This demo uses Chiasm eye tracking.</p>"],
show_clickable_nav: true,
};
const trial_1 = {
type: jsPsychImageKeyboardResponse,
stimulus: "https://images.unsplash.com/photo-1529156069898-49953e39b3ac?w=800",
choices: ["y", "n"],
stimulus_width: 800,
trial_duration: 3000,
prompt: '<p>Do you like this image? Press "y" or "n".</p>',
};
const trial_2 = {
type: jsPsychImageKeyboardResponse,
stimulus: "https://images.unsplash.com/photo-1529333166437-7750a6dd5a70?w=800",
choices: ["y", "n"],
stimulus_width: 800,
trial_duration: 3000,
prompt: '<p>Do you like this image? Press "y" or "n".</p>',
};
jsPsych.run([preload, welcome, trial_1, trial_2]);
</script>
</html>
Step 1 — Load the Chiasm scripts
Add four script tags after the <body> element. You need:
- The Chiasm tracker — the core eye-tracking library. It creates a UI root and appends it to
document.bodywhen the script runs, so this tag has to come after<body>. extension-chiasm— records gaze for the trials you attach it to.plugin-chiasm-setup— initializes the tracker.plugin-chiasm-calibrate— runs calibration.
These URLs pin the tracker and the contrib packages to the versions this tutorial was written against:
<body></body>
<!-- Chiasm tracker. Load after <body> so it can append its UI. -->
<script src="https://cdn.chiasm.eu/v0.6.2/chiasm-tracker.v0.6.2.js"></script>
<!-- jsPsych contrib -->
<script src="https://unpkg.com/@jspsych-contrib/extension-chiasm@0.1.0"></script>
<script src="https://unpkg.com/@jspsych-contrib/plugin-chiasm-setup@0.1.0"></script>
<script src="https://unpkg.com/@jspsych-contrib/plugin-chiasm-calibrate@0.1.0"></script>
Step 2 — Define your credentials and IDs
Inside your script, add the auth token you copied from the Chiasm dashboard, together with your experiment and participant IDs. saveData tells the Chiasm backend to persist the session.
const CHIASM_CONFIG = {
experimentId: "YOUR_EXPERIMENT_ID",
participantId: "YOUR_PARTICIPANT_ID",
authToken: "YOUR_AUTH_TOKEN",
saveData: true,
};
Step 3 — Register the extension
Pass CHIASM_CONFIG when you create the jsPsych instance. The extension stores this configuration and uses it when the setup trial starts the tracker.
const jsPsych = initJsPsych({
default_iti: 250,
extensions: [{ type: jsPsychExtensionChiasm, params: CHIASM_CONFIG }],
on_finish: function () {
jsPsych.data.displayData();
}
});
Step 4 — Add setup and calibration trials
Add two timeline nodes before your experimental trials:
jsPsychChiasmSetupinitializes the tracker and opens a recording session. The participant sees a short loading message.jsPsychChiasmCalibrateruns the ceremony: screen-size calibration, then the camera check, gaze calibration, validation, and handoff back to your timeline.screen_calibration: trueincludes the credit-card screen-size step.
const setup = { type: jsPsychChiasmSetup };
const calibrate = {
type: jsPsychChiasmCalibrate,
screen_calibration: true,
};
Step 5 — Record gaze on each trial
Add the extension to every trial where you want gaze data. Recording starts when the trial loads and stops when the trial ends. event_id labels that recording segment (for example, the stimulus name).
const trial_1 = {
type: jsPsychImageKeyboardResponse,
stimulus: "https://images.unsplash.com/photo-1529156069898-49953e39b3ac?w=800",
choices: ["y", "n"],
stimulus_width: 800,
trial_duration: 3000,
prompt: '<p>Do you like this image? Press "y" or "n".</p>',
extensions: [
{ type: jsPsychExtensionChiasm, params: { event_id: "image_1" } },
],
};
Add the extension only on trials where you want gaze data. Instruction screens, the preload trial, and debrief pages can stay as they are.
Step 6 — Finalize in on_finish
Update the jsPsych on_finish callback. It does four things:
- Matches gaze data to trials —
ensureAllPredictionsReturned()waits for predictions still in flight, then attaches them to the jsPsych trial that produced them. - Displays results —
jsPsych.data.displayData()shows the trial data, including the matched gaze predictions. - Saves a local copy —
localSavedownloads the same dataset as JSON. - Cleans up —
cleanup()releases the tracker. It is safe to call more than once.
Because these calls are asynchronous, make the callback async.
const jsPsych = initJsPsych({
default_iti: 250,
extensions: [{ type: jsPsychExtensionChiasm, params: CHIASM_CONFIG }],
on_finish: async function () {
await jsPsych.extensions.chiasm.ensureAllPredictionsReturned();
jsPsych.data.displayData();
jsPsych.data.get().localSave("json", "chiasm_experiment_data.json");
try {
await jsPsych.extensions.chiasm.cleanup();
} catch (err) {
console.error("extension-chiasm: cleanup error", err);
}
},
});
Step 7 — Update the timeline
Insert setup and calibrate after the welcome screen and before the first experimental trial:
jsPsych.run([preload, welcome, setup, calibrate, trial_1, trial_2]);
The participant sees the instructions, then the calibration flow once, then the image trials with gaze recording active.
Step 8 — Keep the stimulus from shifting (optional)
This step is optional. A scrollbar that appears mid-trial moves the stimulus on screen, so the recorded gaze no longer lines up with what the participant saw. Clip overflow while trials are on screen, and turn scrolling back on for the data table that displayData() injects.
<style>
html, body {
background-color: white;
}
html, body,
.jspsych-content-wrapper,
.jspsych-display-element,
.jspsych-content {
overflow: hidden !important;
}
html:has(#jspsych-data-display),
body:has(#jspsych-data-display) {
overflow: auto !important;
height: auto;
min-height: 100%;
}
.jspsych-content-wrapper:has(#jspsych-data-display),
.jspsych-display-element:has(#jspsych-data-display),
.jspsych-content:has(#jspsych-data-display) {
overflow: auto !important;
max-height: none;
height: auto;
}
</style>
Complete Code
Putting it all together. Copy the HTML below into an index.html file, replace the placeholders with your dashboard values, then serve it locally (e.g. npx serve or python -m http.server).
Opening the file directly via file:// will not work — the webcam requires a secure context (https:// or http://localhost).
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Chiasm jsPsych contrib demo</title>
<!-- jsPsych core + plugins. Safe to load in <head>. -->
<script src="https://unpkg.com/jspsych@8.2.2"></script>
<link href="https://unpkg.com/jspsych@8.2.2/css/jspsych.css" rel="stylesheet" type="text/css" />
<script src="https://unpkg.com/@jspsych/plugin-image-keyboard-response@2.1.0"></script>
<script src="https://unpkg.com/@jspsych/plugin-preload@2.1.0"></script>
<script src="https://unpkg.com/@jspsych/plugin-instructions@2.1.0"></script>
<style>
/* Clip overflow during trials so a too-tall image does not add a scrollbar
(a scrollbar would shift the stimulus and recorded gaze bounds). */
html, body {
background-color: white;
}
html, body,
.jspsych-content-wrapper,
.jspsych-display-element,
.jspsych-content {
overflow: hidden !important;
}
/* jsPsych.data.displayData() injects #jspsych-data-display — allow that dump to scroll. */
html:has(#jspsych-data-display),
body:has(#jspsych-data-display) {
overflow: auto !important;
height: auto;
min-height: 100%;
}
.jspsych-content-wrapper:has(#jspsych-data-display),
.jspsych-display-element:has(#jspsych-data-display),
.jspsych-content:has(#jspsych-data-display) {
overflow: auto !important;
max-height: none;
height: auto;
}
</style>
</head>
<body></body>
<!--
The Chiasm tracker auto-creates a UI root and appends it to document.body
on script evaluation, so it MUST load after the <body> element exists.
-->
<script src="https://cdn.chiasm.eu/v0.6.2/chiasm-tracker.v0.6.2.js"></script>
<script src="https://unpkg.com/@jspsych-contrib/extension-chiasm@0.1.0"></script>
<script src="https://unpkg.com/@jspsych-contrib/plugin-chiasm-setup@0.1.0"></script>
<script src="https://unpkg.com/@jspsych-contrib/plugin-chiasm-calibrate@0.1.0"></script>
<script>
const CHIASM_CONFIG = {
experimentId: "YOUR_EXPERIMENT_ID",
participantId: "YOUR_PARTICIPANT_ID",
authToken: "YOUR_AUTH_TOKEN",
saveData: true,
};
const jsPsych = initJsPsych({
default_iti: 250,
extensions: [{ type: jsPsychExtensionChiasm, params: CHIASM_CONFIG }],
on_finish: async function () {
await jsPsych.extensions.chiasm.ensureAllPredictionsReturned();
jsPsych.data.displayData();
jsPsych.data.get().localSave("json", "chiasm_experiment_data.json");
try {
await jsPsych.extensions.chiasm.cleanup();
} catch (err) {
console.error("extension-chiasm: cleanup error", err);
}
},
});
const preload = { type: jsPsychPreload, auto_preload: true };
const welcome = {
type: jsPsychInstructions,
pages: ["<p>Welcome. This demo uses Chiasm eye tracking.</p>"],
show_clickable_nav: true,
};
const setup = { type: jsPsychChiasmSetup };
const calibrate = {
type: jsPsychChiasmCalibrate,
screen_calibration: true,
};
const trial_1 = {
type: jsPsychImageKeyboardResponse,
stimulus: "https://images.unsplash.com/photo-1529156069898-49953e39b3ac?w=800",
choices: ["y", "n"],
stimulus_width: 800,
trial_duration: 3000,
prompt: '<p>Do you like this image? Press "y" or "n".</p>',
extensions: [
{ type: jsPsychExtensionChiasm, params: { event_id: "image_1" } },
],
};
const trial_2 = {
type: jsPsychImageKeyboardResponse,
stimulus: "https://images.unsplash.com/photo-1529333166437-7750a6dd5a70?w=800",
choices: ["y", "n"],
stimulus_width: 800,
trial_duration: 3000,
prompt: '<p>Do you like this image? Press "y" or "n".</p>',
extensions: [
{ type: jsPsychExtensionChiasm, params: { event_id: "image_2" } },
],
};
jsPsych.run([preload, welcome, setup, calibrate, trial_1, trial_2]);
</script>
</html>
What Changed — Summary
Here is a quick recap of every change made to the original experiment:
| # | Change | Why |
|---|---|---|
| 1 | Loaded chiasm-tracker.v0.6.2.js after <body>, plus extension-chiasm, plugin-chiasm-setup, and plugin-chiasm-calibrate at 0.1.0 | Loads the tracker and the jsPsych contrib packages |
| 2 | Defined CHIASM_CONFIG with experimentId, participantId, authToken, and saveData | Credentials for the Chiasm service |
| 3 | Registered jsPsychExtensionChiasm on initJsPsych | Makes the extension available for the rest of the timeline |
| 4 | Added jsPsychChiasmSetup and jsPsychChiasmCalibrate | Initializes the tracker, then runs screen-size and gaze calibration |
| 5 | Attached jsPsychExtensionChiasm to each image trial with an event_id | Records gaze for that trial and tags the segment |
| 6 | Made on_finish async and called ensureAllPredictionsReturned(), displayData(), localSave(), and cleanup() | Matches gaze data to trials, shows and saves the result, then releases the tracker |
| 7 | Inserted setup and calibrate into the timeline | Runs calibration after the welcome screen and before the first image |
| 8 | Clipped page overflow during trials (optional) | Keeps a scrollbar from shifting the stimulus and the recorded gaze bounds |
Next Steps
- Get the Data — learn how to access gaze predictions from the dashboard
- Custom Code tutorial — for experiments that do not use jsPsych
startRecordingreference — how event IDs tag a recording segment- Legacy jsPsych helper — the previous local-helper integration