LightMill helps you build web experiments, such as human-computer interaction or psychology studies, and collect their data on your own server. You write each part of the experiment as a task, list the tasks in a timeline, and LightMill shows them in turn, sends what participants do to a log server, and exports it as CSV.
participant's browser your server
timeline ──▶ TimelinePlayer ──▶ logger ───── HTTPS ─────▶ log server ──▶ SQLite ──▶ CSV
(tasks) (shows tasks) (sends logs)- An experiment is a study, created once on the server.
- A run is one participant going through the experiment once. Each run has a name, usually derived from the participant. That name is unique among runs that are not canceled in the same experiment.
- A timeline is the sequence of tasks of a run. A task is a plain object with a
type, such as{ type: 'trial', size: 'large' }, and your app has one component per task type. - A log records something that happened during a run, such as an answer and its reaction time. Logs have a
typeand values, and are numbered in order. - A run is
runningonce started, thencompleted,canceled, orinterrupted. A running or interrupted run can be resumed: the participant continues after the last task whose log reached the server. - The host is the researcher's account on the server. It can read every log. Participants only see their own runs.
| You want to | Use |
|---|---|
| Show tasks with React | @lightmill/react-experiment |
| Show tasks without React | @lightmill/runner |
| Send logs from the browser | @lightmill/log-client |
| Store logs and export them | @lightmill/log-server |
| Order conditions across participants | @lightmill/counterbalancing |
| Turn a Touchstone 1 design into timelines | @lightmill/convert-touchstone |
| Write another client or server for the log server | @lightmill/log-api |
A typical React experiment uses react-experiment and log-client in the app, and log-server on the server. Each package also works on its own: TimelinePlayer doesn't need a server, and log-client works with any interface.
A task component reads its task, logs what happened, and says when it is done:
import {
TimelinePlayer,
useLogger,
useTask,
} from '@lightmill/react-experiment';
function Question() {
const { task, onTaskCompleted } = useTask('question');
const log = useLogger('answer');
return (
<button
onClick={() => {
log({ taskId: task.id, answer: 'yes' });
onTaskCompleted();
}}
>
Yes
</button>
);
}
const timeline = [
{ type: 'question', id: 'q1' },
{ type: 'question', id: 'q2' },
];
export function App() {
return (
<TimelinePlayer
timeline={timeline}
onLog={async (log) => console.log(log)}
elements={{
tasks: { question: <Question /> },
completed: <p>Thank you!</p>,
}}
/>
);
}Here onLog only prints each log. With @lightmill/log-client, it sends them to your log server instead: the getting started guide builds a complete experiment around this.
- Getting started: build a complete experiment, from the first task to the CSV file.
- Deploying: put the app and the log server online.
- Resuming runs: let participants continue after a reload.
- Exporting data: get the logs out, and what the CSV contains.
- Troubleshooting: find what's wrong when the app, the server, or saving fails.
@lightmill/log-client supports Chrome and Edge 85, Firefox 90, and Safari 15 or later. @lightmill/react-experiment needs React 19.2 or later. The log server needs Node.js 24.12 or later.
See CONTRIBUTING.md.
MIT