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.
When to use a custom HTML trial
Section titled “When to use a custom HTML trial”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.
Setting up the trial
Section titled “Setting up the trial”Two things make an app trial:
type,stimFormat,stimtest,html,my_trialThe stim value resolves as follows:
- A plain name is a relative path into your project’s stimuli folder, and
.htmlis appended automatically when missing, somy_trialandmy_trial.htmlboth loadmy_trial.htmlwhen the project runs (the editor’s validation is stricter; see thestimpage). - A value starting with
httpis loaded as an externally hosted app. %variable%placeholders are substituted first, so you can pass data into the app through query parameters, for exampleindex.html?a=%response%, and read them withnew URLSearchParams(window.location.search).
Uploading the app’s files
Section titled “Uploading the app’s files”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 in app API essentials
Section titled “The in app API essentials”The full API lives on the stimFormat page; these are the parts every app needs.
Saving the response (required)
Section titled “Saving the response (required)”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 } }}, "*");Reaction time is coarser
Section titled “Reaction time is coarser”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.
Which columns do not apply
Section titled “Which columns do not apply”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.
Reading project data from inside the app
Section titled “Reading project data from inside the app”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.
Minimal working example
Section titled “Minimal working example”Trial file row:
type,stimFormat,stimtest,html,my_trialmy_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.
Next steps
Section titled “Next steps”- The
stimFormatcolumn 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