Sidhanshu MongaContribution
GitHub↗
/ Engineering contribution — C.05Independent
← All contributions

Interfaces that adapt to what a user already knows

A per-element model of how familiar a user is with a control, and how much it is costing them — scored in the browser from interaction alone, with no backend and no model required.

Organization
Independent
Dates
2025 — present · core v0.3.0
Status
Open source (MIT) · maintained · no runtime dependencies, no backend

The problem

Every product ships the same interface to someone on their first day and to someone who has used it daily for three years. The standard remedy is a scatter of booleans — hasSeenExportTooltip, dismissedOnboarding — one per feature, added in a hurry and never removed.

Those flags carry no degree and no distress signal. A user who has clicked something once and a user who has clicked it four hundred times are both “seen”, and a user who hovers, hesitates and gives up looks identical to one who never arrived. The thing a product actually wants to know has no representation at all.

My role

Sole author across three packages: the scoring engine and storage layer in @mindra.dev/core, the provider, components and hooks in @mindra.dev/react, and a browser extension. Core takes no runtime dependencies; the React package depends only on core.

The approach

Replace the flags with one question, asked per element rather than per feature: how familiar is this user with this specific control, right now?

const { expertise, friction, suggestion } = useAdaptive('export-btn')

Familiarity is continuous, so it has a degree. Friction is measured alongside it, so struggle is visible rather than inferred. And both are derived from interaction the runtime can already observe — hover, click, focus, hesitation, abandonment — so a product gets the signal without anyone instrumenting a funnel for it.

Two constraints shaped the rest. The engine is deterministic: the scores are arithmetic, and no model is required for any of it. And it is local-first: there is no default network call, which is what makes it adoptable without a vendor review.

Architecture

Four scores, each a closed-form expression over counts and timings:

  • Familiarity 1 − e^(−clicks / λ), λ = 8 by default. Asymptotic, so it rises quickly out of novice and never quite reaches certainty — the curve is the claim that the twentieth use teaches less than the second.
  • Friction a weighted sum of hesitation, abandonment and reported errors (0.4 / 0.4 / 0.2). Hesitation is the gap between the pointer arriving and the element being activated, clamped so one abandoned tab cannot poison a score.
  • Confidence (1 − friction) × familiarity. Deliberately multiplicative: heavy use with heavy struggle is not expertise.
  • Tier a bucketing of familiarity at 0.28 / 0.58 / 0.85 into novice, learning, proficient and expert, plus a suggested treatment.

The resolution order for adaptive content is where most of the design judgement sits. Variants walk down to the nearest supplied tier, so specifying only novice and expert leaves a proficient user on the novice copy. The interface does not simplify until it is told it may — the failure mode of guessing wrong is a confused user, so the default is the more explanatory version.

Implementation

Setup is a provider and a wrapper, and the runtime does the observation:

<AdaptiveProvider appId="my-app">{children}</AdaptiveProvider>

<Adaptive id="export" novice="Export this project as a PDF" expert="Export">
  <button>Export</button>
</Adaptive>

It also sets data-adaptive-tier on the element, so transitions are driven from the host stylesheet rather than by a library writing inline styles — the difference between a tool that participates in a design system and one that fights it.

Storage is localStorage under a per-app key, capped at 500 elements with coldest-first eviction. Element identifiers derived from the DOM are scrubbed of emails, UUIDs, long hashes and digit runs before they are written, so order-1048577 is recorded as order-#. What is kept is timings and counts: never keystrokes, never input values, never page content.

AI is strictly optional and never proxied. If generated copy is wanted instead of hand-written variants, the host brings its own key — ollama, openai, gemini, window.ai, a custom runner, or their own endpoint — and results are cached per element and tier, so a model is consulted once per variation rather than once per interaction.

Sources

Public references, so none of the above has to be taken on my word.