Skip to content

Custom HTML Stimuli

Experiments

If you are comfortable writing HTML and JavaScript, Testable can hand an entire trial over to a page of your own. Setting stimFormat to html turns the row into an app trial: your page is loaded into an iframe, runs whatever interaction you have built, and reports its result back to Testable, which records it and moves on to the next trial.

This is the escape hatch for interactions the built in trial types cannot express. It is also the most work, so reach for it last.

Prefer the built ins first. Keyboard, button and click responses cover reaction time tasks; the responseType widgets (slider, box, dropdown, likert, rank, stars) plus type = form cover surveys; the stimFormat media types cover images, audio and video. See Responses in Experiments and Forms and Surveys.

Resort to a custom HTML trial when no built in expresses the interaction:

  • Drag and drop tasks
  • Canvas drawing, games, gesture or free drawing input
  • Multi step logic inside a single trial
  • Rich third party widgets
  • Embedding an externally hosted web app

The trade off: a custom HTML trial gives up Testable’s automatic handling. Its recorded reaction time is coarser (see below), the key scoring, responseWindow, presTime, feedback and keyboard or button response columns do not apply, and your app must save a response itself or the trial never advances.

Two things make an app trial:

  1. stimFormat set to html.
  2. The app reference in the stim column (preferred) or stim1 (fallback).
type,stimFormat,stim
test,html,my_trial

The stim value resolves as follows:

  • A plain name is a relative path into your project’s stimuli folder, and .html is appended automatically when missing, so my_trial and my_trial.html both load my_trial.html when the project runs (the editor’s validation is stricter; see the stim page).
  • A value starting with http is loaded as an externally hosted app.
  • %variable% placeholders are substituted first, so you can pass data into the app through query parameters, for example index.html?a=%response%, and read them with new URLSearchParams(window.location.search).

Upload the app’s files (the HTML page plus any CSS, JS and media it uses) to the project’s Stimuli section before running. Uploaded HTML, CSS and JS files land in the same stimuli folder as your media stimuli, so they can reference each other with bare relative paths such as styles.css, script.js or image1.png.

The full API lives on the stimFormat page; these are the parts every app needs.

Your page must call:

parent.testableSDK.saveHtmlTrialResponse('Button clicked', { time_taken: 15, confidence: 'high' });

This records the first argument as the trial’s response, closes the app, and advances to the next trial. If the app never calls it, the trial never advances, so wire it to whatever counts as “done” in your interaction.

The optional second argument saves custom variables: each key becomes its own column in the exported results CSV, filled on that trial’s row. Keep the values primitive (strings or numbers); nested objects are exported as [object Object].

Externally hosted apps cannot call parent.testableSDK directly (cross origin); they post a message instead, which Testable forwards to the same save:

window.parent.postMessage({
testable_app_result: { response: "Button clicked", custom_variables: { score: 85 } }
}, "*");

The RT recorded for an app trial is measured from the moment the trial is scheduled, before the ITI elapses and before your page loads. It therefore includes the ITI wait and the app’s load time, not just time in app. If you need precise timing, measure it yourself inside the app and save it as a custom variable.

The trial waits indefinitely for your app to save; the keyboard and button response columns do not apply, and neither do key scoring, responseWindow, presTime or the feedback columns. Any scoring or per trial timing you need happens in your own code and travels back as custom variables.

The SDK also exposes read access from inside the app, including parent.testableSDK.variables for custom variables saved by earlier trials, parent.testableSDK.allocatedSubjectGroups for the participant’s subject groups, and parent.testableSDK.columnForTrial(columnName, trialIndex) for standard column values. The stimFormat page documents these, along with their sharp edges.

Trial file row:

type,stimFormat,stim
test,html,my_trial

my_trial.html, uploaded to Stimuli:

<button id="go">Click me</button>
<script>
document.getElementById('go').addEventListener('click', function () {
parent.testableSDK.saveHtmlTrialResponse('Button clicked', { time_taken: 15, confidence: 'high' });
});
</script>

Clicking the button records Button clicked as the response, adds time_taken and confidence columns to the results, and advances to the next trial.

  • The stimFormat column page for the complete API: URL resolution details, the postMessage bridge, reading variables and columns, and the known gotchas
  • Scripts for injecting JavaScript into built in trials instead of replacing them
  • Other Visual Content in Trials for simple in cell HTML formatting