Every help widget ships with the same blunt policy: park a launcher in the corner and hope the people who need it click it. But the visitors who most need help — the ones looping through your navigation, clicking things that aren't buttons — are precisely the ones least equipped to find one more UI element. User confusion detection inverts the arrangement: the page notices the struggle and brings help to the visitor, in proportion to how lost they actually are.
ClickStream's Signals API exposes the raw material as visitor.scores.confusion — a 0–100 behavioral score in the public snapshot, computed by one of the collector's 26 scoring models (each CI-benchmarked at p95 under 3 ms per event). This article builds the UX instrument on top of it: a three-tier progressive help ladder, an onVisitor subscription, and a small state machine with hysteresis so the UI never flaps between states. Then the honest part — what a behavioral score can't tell you, and why the thresholds in this post are starting points, not gospel.
What User Confusion Detection Actually Measures
Confusion is not frustration. A frustrated visitor knows what they want and can't get it; a confused visitor doesn't know where to look or what to do next. ClickStream scores them separately — visitor.scores.frustration and visitor.scores.confusion are distinct fields in the snapshot — because they call for different responses. Frustration wants friction removed; confusion wants orientation added.
The confusion model is a pure function over session-level behavioral features. The current signal set, with its default weights (the internal model sums to 0–1; the snapshot scales it to 0–100):
| Signal | Default weight | Fires when |
|---|---|---|
| Directionless navigation | +0.25 / +0.10 | Navigation entropy above 2.5 (strong) or 1.5 (moderate) — random, loop-like browsing |
| Rapid scanning | +0.25 | More than 3 pages with average scroll depth under 20% and under 10 s per page |
| Dead clicks | +0.20 | Clicking elements that aren't interactive |
| Hesitation | +0.15 | Over 30 s on a page with maximum scroll depth under 20% |
| Rage clicks | +0.15 | Rapid repeated clicking — compounds confusion |
Two details in that table are worth pausing on. First, the hesitation signal is gated to web sessions: a native mobile app can't emit browser scroll depth, so a long-dwell screen would read as zero scroll — structural absence of data, not confusion — and the model deliberately skips the penalty there. Second, every weight and threshold is a tunable default, not a constant; more on that under the honest limits below.
Collector-side, the model also classifies the confusion into a type — lost, overwhelmed, searching, or stuck — which powers the dashboard's confusion-hotspot views. Page code sees the scalar plus visitor.scores.emotionalState, whose eight states include confused. The full model treatment, including the four types and the ethics boundaries around emotional inference, is in Detecting Confusion and Emotional State from Behavioral Signals; how the categorical fields behave alongside the scalars is covered in Beyond the Score: Emotional State and Decision Stage.
Reading the Confusion Score from Page Code
The read side is @clickstreamhq/signals, currently a developer preview on the 0.1.0-alpha line. As with every Signals read, configure({ apiKey }) comes first — a cs_live_ browser key, which is domain-gated to the origins you've verified — and getVisitorOrNull() is the fail-open entry point:
import { configure, getVisitorOrNull } from '@clickstreamhq/signals';
configure({
apiKey: 'cs_live_xxx',
endpoint: 'https://t.example.com', // your verified first-party tracking domain
});
const visitor = await getVisitorOrNull(); // null when Signals can't answer
if (visitor && !visitor.bot.isBot) {
visitor.scores.confusion; // 0-100 — lost, overwhelmed, searching, stuck
visitor.scores.frustration; // 0-100; 60+ = struggling
visitor.scores.emotionalState; // 'confused' is one of the 8 states
}
A one-shot read like this answers "how lost is this visitor right now" in a single HTTPS round trip, documented at roughly 50–150 ms. But confusion is a trajectory, not a moment — a visitor who was fine at page load can be thoroughly lost ninety seconds later. Help UI needs the subscription form, onVisitor(), which fires immediately with the first snapshot and then polls (default every 2,000 ms, floor 1,000). If any of this surface is new, the getting-started guide walks the whole read API.
The Progressive Help Ladder
The design principle: help escalates in proportion to evidence, and only ever adds. No tier hides content, moves controls, or interrupts. Three tiers cover most sites:
| Tier | Enters when | Exits when | What the visitor sees |
|---|---|---|---|
calm | Default | — | The standard page. No help UI at all. |
hints | confusion ≥ 50 | confusion < 35 | Inline hints appear: contextual tips, expanded form-field guidance, a "the short version" summary on docs pages |
offer | confusion ≥ 70 and frustrated (≥ 60) or emotionalState === 'confused' | confusion < 55, after a minimum dwell | A visible, dismissible offer of live help — never an auto-opened widget |
The top tier requires corroboration on purpose. A high confusion score alone earns hints; the live-help offer waits for a second signal — the frustration score crossing the isFrustrated() threshold of 60, or the emotional-state model independently classifying the session as confused. One model having a noisy minute shouldn't summon a human.
Docs Pages: Surface the Short Version
On documentation, moderate confusion usually means the page assumed context the reader doesn't have. The cheapest intervention is a pre-written simplified summary — three sentences and a link to prerequisites — that stays display: none until the hints tier reveals it at the top of the article. Readers who arrive oriented never see it; readers who are rapid-scanning through four docs pages in forty seconds get the on-ramp.
Forms: Expand the Field-Level Guidance
Forms are where the stuck pattern concentrates — dead clicks on labels, long dwell with no scroll. The same tier mechanism can swap terse placeholder text for real field-level guidance: format examples under each input, a "where do I find this?" expander next to the tax-ID field. Progressive disclosure is the polite default here anyway; confusion-awareness just decides when the disclosure earns its screen space.
A Hysteresis State Machine That Never Flaps
The naive implementation — if (confusion > 50) showHints() re-evaluated every poll — is a flapping machine. Scores move with every scored event; a visitor hovering around 50 would watch hints blink in and out at poll cadence, which is itself confusing. The fix is hysteresis, borrowed from thermostats: the threshold to enter a tier sits well above the threshold to leave it, and de-escalation additionally waits out a minimum dwell.
import { configure, onVisitor, isFrustrated } from '@clickstreamhq/signals';
configure({ apiKey: 'cs_live_xxx', endpoint: 'https://t.example.com' });
// Hysteresis: enter thresholds sit well above exit thresholds, so a
// score oscillating around a boundary can't flap the UI.
const ENTER = { hints: 50, offer: 70 };
const EXIT = { hints: 35, offer: 55 };
const MIN_TIER_MS = 15000; // never retract help sooner than 15s after showing it
let tier = 'calm';
let tierSince = 0;
const sub = onVisitor((visitor) => {
// Placeholder or bot data must never drive the help UI.
if (visitor.stale || visitor.pending || visitor.bot.isBot) return;
const confusion = visitor.scores.confusion; // 0-100
const struggling =
isFrustrated(visitor) || visitor.scores.emotionalState === 'confused';
if (tier !== 'offer' && confusion >= ENTER.offer && struggling) {
setTier('offer');
} else if (tier === 'calm' && confusion >= ENTER.hints) {
setTier('hints');
} else if (tier === 'offer' && confusion < EXIT.offer && settled()) {
setTier('hints');
} else if (tier === 'hints' && confusion < EXIT.hints && settled()) {
setTier('calm');
}
});
function settled() {
return Date.now() - tierSince >= MIN_TIER_MS;
}
function setTier(next) {
tier = next;
tierSince = Date.now();
document.documentElement.dataset.helpTier = next; // CSS takes it from here
}
The subscription writes one attribute on <html> and CSS does the rest — which keeps the behavioral logic in one auditable place and makes every intervention a plain stylesheet rule:
/* Default: no help UI. Tiers only ever reveal — never hide content. */
.inline-hint, .field-help, .docs-summary, .live-help-offer { display: none; }
[data-help-tier='hints'] .inline-hint,
[data-help-tier='hints'] .field-help,
[data-help-tier='hints'] .docs-summary { display: block; }
[data-help-tier='offer'] .inline-hint,
[data-help-tier='offer'] .field-help,
[data-help-tier='offer'] .docs-summary,
[data-help-tier='offer'] .live-help-offer { display: block; }
Three properties of this machine are load-bearing. Escalation is instant, de-escalation is slow: a jump to offer happens on the first qualifying snapshot, but stepping down requires both a sub-exit-threshold score and fifteen settled seconds, so help never vanishes mid-reach. Suspect data is inert: snapshots flagged stale or pending — including the zeroed placeholder contexts the server returns while a visitor is initializing — and anything with bot.isBot simply don't move the machine. And the machine only descends one rung at a time, so the visible transitions are always gentle. If you're in a component tree, the same pattern ports directly onto the React hooks with the state machine in a useEffect; if a human should follow the machine into the loop, routing frustrated visitors to support with context is the companion pattern.
The Honest Limits
It's Behavioral Inference, Not Telepathy
The model reads behavior, and some innocent behavior is confusion-shaped. A visitor slowly reading a dense legal page above the fold can resemble hesitation; a researcher deliberately opening six product pages can brush against the scanning heuristic. The score is a well-calibrated prior, not a verdict — which is exactly why the ladder's interventions are additive-only and dismissible. A false positive costs one unnecessary hint, not a hijacked session.
Thresholds Are Per-Site Numbers
Enter-at-50, offer-at-70 are defensible defaults, not universals. A three-field SaaS signup and a 400-SKU parts catalog have wildly different baseline navigation entropy, so identical thresholds produce very different intervention rates. Watch your tier-transition counts for a week and tune. The model itself is tunable the same way — every signal weight and threshold in the table above is a per-site override, the mechanics of which are covered in Per-Site Scoring Weights.
Fail Open, and Never Weaponize It
When Signals can't answer — blocked, rate-limited past the stale window, coverage exhausted — getVisitorOrNull() returns null, the subscription goes quiet, and the machine holds at calm: your page, unchanged. That's the standard fail-open contract, and a help ladder inherits it for free because the default tier is the unmodified page.
The other guardrail is intent. A confusion score is an assistance signal, full stop. Using it to pressure — countdown timers for the overwhelmed, score-gated pricing, urgency copy targeted at people who've lost the thread — is the dark-pattern inversion of everything above, and it's the reason the ladder's rules only ever add clarity. The test for any tier is simple: would the intervention still make sense if the visitor could see the score that triggered it?
The Bottom Line
visitor.scores.confusionis a 0–100 public snapshot field — built from navigation entropy, rapid scanning, dead clicks, hesitation, and rage clicks.- Escalate proportionally: inline hints at moderate confusion; a live-help offer only when high confusion is corroborated by frustration or a
confusedemotional state. - Use hysteresis: enter high, exit low, de-escalate slowly. A help UI that flaps generates the confusion it claims to treat.
- Ignore
stale,pending, and bot snapshots; hold atcalmwhen Signals can't answer. - Tune per site, only ever add clarity, and never point a struggle signal at someone's wallet.
The best help UI is invisible to everyone who doesn't need it — and unmissable, without being unignorable, to everyone who does.