Definition
A system that responds to a user inside roughly 400 milliseconds keeps the user engaged; slower than that and the user disengages, multitasks, or drops the session. Speed is not just performance — it is the single most reliable lever for sustained attention. Below the threshold, work and tool merge; above it, the human waits.
Why it matters for ShurIQ reports
A viz-hub viewport that hangs for two seconds while it computes a re-rank loses the executive in real time. Skeleton states, optimistic UI on filter changes, and chart-by-chart streaming render are how a dense intelligence brief stays inside the threshold. Perceived performance matters more than measured performance — a 350ms paint with motion always beats a 250ms paint that looks frozen.
Takeaways
- Acknowledge every user input within 100ms even if the result lags — silence reads as broken.
- Use skeleton frames and progressive paint to keep the viewport alive during data fetches.
- A deliberate 200ms hold on a calculation can raise perceived value, but only on outputs the reader views as analytical (scores, rankings).
- Progress bars work even when imprecise; uncertainty is tolerable, blank screens are not.
- Profile the third interaction in a session, not the first — first paints lie.
Visual motion language
Every interaction returns a sub-100ms acknowledgement (subtle scale-pulse on press), then a sequence-reveal of content as data lands. Numbers count-up rather than appearing instantly, signaling computation.
Origins
Walter J. Doherty and Arvind J. Thadani, 1982 — IBM Systems Journal study replacing the prior 2-second standard.
Cavalry scene
The script below builds this concept's motion in Cavalry through the Stallion bridge. Pipe to cavalry_run_script via MCP, or paste into Cavalry's JavaScript Editor.
// Laws of UX · doherty-threshold · Cavalry scene
// Motion family: center-out radial pulse
// Palette: deep magenta + cream
// To run: pipe to cavalry_run_script tool, or paste into Cavalry's JavaScript Editor
// Built 2026-04-30 by ShurAI
(function () {
var PREFIX = "claude_lawofux_doherty-threshold_";
var existing = api.getAllSceneLayers();
for (var i = 0; i < existing.length; i++) {
try {
var nm = api.getNiceName(existing[i]);
if (nm && nm.indexOf("claude_lawofux_") === 0) api.deleteLayer(existing[i]);
} catch (e) {}
}
var BG = "#5A1E4D";
var DARK1 = "#4A1740";
var DARK2 = "#6E2A60";
var CREAM = "#D6CDB0";
var bg = api.primitive("rectangle", PREFIX + "bg");
api.set(bg, { "generator.dimensions": [1080, 1080] });
api.setFill(bg, true);
api.set(bg, { "material.materialColor": BG });
// Concentric circles
var rings = [
{ name: "r1", radius: 250, color: DARK1 },
{ name: "r2", radius: 190, color: DARK2 },
{ name: "r3", radius: 130, color: DARK1 },
{ name: "r4", radius: 70, color: DARK2 }
];
var ringIds = [];
for (var i = 0; i < rings.length; i++) {
var r = rings[i];
var e = api.primitive("ellipse", PREFIX + "ring_" + r.name);
api.set(e, {
"generator.radius": [r.radius, r.radius],
"position.x": 0, "position.y": 0,
"opacity": 0
});
api.setFill(e, true);
api.set(e, { "material.materialColor": r.color });
var inF = i * 8;
var doneF = inF + 14;
api.keyframe(e, inF, { "opacity": 0 });
api.keyframe(e, doneF, { "opacity": 50 });
// Doherty pulse: 400ms cycle = ~10 frames @ 24fps
api.keyframe(e, 60, { "scale.x": 1.0, "scale.y": 1.0 });
api.keyframe(e, 65, { "scale.x": 1.04, "scale.y": 1.04 });
api.keyframe(e, 70, { "scale.x": 1.0, "scale.y": 1.0 });
api.keyframe(e, 80, { "scale.x": 1.0, "scale.y": 1.0 });
api.keyframe(e, 85, { "scale.x": 1.04, "scale.y": 1.04 });
api.keyframe(e, 90, { "scale.x": 1.0, "scale.y": 1.0 });
ringIds.push(e);
}
// Cream pupil
var pupil = api.primitive("ellipse", PREFIX + "pupil");
api.set(pupil, {
"generator.radius": [30, 30],
"position.x": 0, "position.y": 0,
"opacity": 0
});
api.setFill(pupil, true);
api.set(pupil, { "material.materialColor": CREAM });
api.keyframe(pupil, 36, { "opacity": 0 });
api.keyframe(pupil, 50, { "opacity": 100 });
var layerCount = api.getAllSceneLayers().length;
console.log("scene built: doherty-threshold (" + layerCount + " layers)");
})();