# intent-link: complete reference > Target prediction for React / Next.js. Version 1.0.13 · MIT. ## Install npm install intent-link ## Public contract - IntentProvider creates and runs one shared intent engine. Mount it once near the application root. - IntentLink wraps Next.js Link, forwards link and anchor props, and adds importance, cost, and onIntent. - useIntentTarget returns a callback ref for observing any HTML element. - onIntent has the type () => void. It does not expose a URL, probability, utility, or physics state. - Public types are IntentLinkProps, IntentLevel, and UseIntentTargetOptions. - Defaults are importance="medium" and cost="low". - IntentLink always sets Next.js prefetch={false}. Predictive prefetching is opt-in through onIntent={() => router.prefetch(href)}. ## Implementation details ### Provider and target lifecycle IntentProvider creates one IntentEngine, starts it in a React effect, and disposes it during cleanup. A nested provider produces a development warning. Disposal removes global listeners, disconnects the observer, clears visible targets, and cancels any scheduled animation frame. useIntentTarget gives each target a stable React useId. It registers the current HTML element only when an onIntent callback exists. It updates importance, cost, and the latest callback without exposing engine state to React consumers. Changing or unmounting the element unregisters it. Calling the hook outside IntentProvider produces a warning. ### Input and scheduling The engine checks matchMedia("(pointer: coarse)") when it starts. Fine-pointer devices listen to passive mousemove events. Coarse-pointer devices listen to passive scroll events. The first relevant event initializes the motion estimator and wakes the engine. Calculations run in requestAnimationFrame, not through React rendering. After input stops, the loop continues briefly and then sleeps after 500 milliseconds. A newly visible target can wake the loop only while pointer or scroll input is still active. ### Kalman filtering Browser movement events are noisy. A Kalman filter smooths that noise and estimates motion from recent samples. Desktop uses a two-dimensional filter for pointer position and velocity. Mobile uses a one-dimensional filter for vertical scrolling. The engine also reads velocity variance from the filter as its uncertainty estimate and clamps it to at least 1e-8 for numerical safety. The Kalman filter is an internal measurement tool. Consumers cannot configure it and do not receive its state. ### Visibility and high-density pages An IntersectionObserver tracks which registered targets intersect the viewport. Only intersecting targets enter the active calculation set. Each frame also verifies that a target is genuinely visible and has usable geometry, so elements hidden by responsive CSS do not participate merely because they still exist in the DOM. When a target leaves the viewport, it is removed from active calculations and its one-shot trigger is reset. When IntersectionObserver is unavailable, registered targets fall back to direct visibility checks. ### Target geometry and energy score For each visible target, the engine measures the target's center and its effective size: effectiveSize = max(target width, target height, 1) On desktop, distance is measured in two dimensions from the pointer to the target center. On mobile, horizontal distance is ignored and vertical distance is measured from a point 55% down the viewport. The implementation forms three negative exponents: kineticEnergyExponentAgent = -(agentVelocity²) / (2 × velocityVariance) kineticEnergyExponentTarget = -(targetVelocity²) / (2 × velocityVariance) potentialEnergyDifferenceExponent = -(π × e × distance²) / effectiveSize² Desktop supplies agent velocity and treats target velocity as zero. Mobile supplies scroll velocity as target velocity and treats agent velocity as zero. Their sum becomes an unnormalized target score: unnormalizedProbability = exp( kineticEnergyExponentAgent + kineticEnergyExponentTarget + potentialEnergyDifferenceExponent ) The naming comes from a thermodynamic model: motion contributes kinetic energy, separation contributes potential-energy difference, and the exponential favors lower-energy states. The gravity comparison is an intuition for targets becoming more attractive as the user settles near them; it is not a literal browser force. ### Desktop and mobile probability Desktop compares all visible targets and includes a constant null state representing "none of these targets": partitionFunction = 1 + sum(all visible unnormalized probabilities) normalizedProbability = unnormalizedProbability / partitionFunction Mobile uses the unnormalized probability directly. This avoids diluting a vertically approached target merely because several links are visible at once. ### Decision, firing, and rearming Importance and cost map to fixed product-level weights: importance: high=1, medium=0.5, low=0.2 cost: high=0.8, medium=0.4, low=0.1 expectedUtility = decisionProbability × importanceWeight - costWeight When expected utility becomes greater than zero, the engine queues onIntent once and locks that target. The lock is released when its decision probability falls below 0.05 or when it leaves the viewport. Callbacks run after the frame calculation and are isolated with error handling so one consumer callback does not break the engine loop. ### Appropriate onIntent work onIntent should start safe, repeatable preparation work: route prefetching, data warming, image preloading, or code loading. Irreversible actions such as purchases, messages, analytics commitments, or form submissions must remain behind an actual user action. ## Customer documentation ## Quickstart Add intent-aware actions to a Next.js app in three steps. ### 1 · Install ```bash npm install intent-link ``` ### 2 · Wrap your app once Mount `IntentProvider` once in your root layout. It runs the shared intent engine for everything inside it. ```tsx // app/layout.tsx import { IntentProvider } from "intent-link" export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children} ) } ``` ### 3 · Use IntentLink Use `IntentLink` like a normal Next.js link and put the work you want to start early inside `onIntent`. ```tsx "use client" import { IntentLink } from "intent-link" import { useRouter } from "next/navigation" function ProductCard({ href }: { href: string }) { const router = useRouter() return ( router.prefetch(href)}> View product ) } ``` > That is all most applications need. The library does not prefetch automatically—you choose what `onIntent` does. --- ## How it works intent-link watches how someone moves through a page and estimates which visible target they are approaching. When the signal is strong enough, it calls that target's `onIntent` function. ### 1 · Observe movement On desktop, the engine reads pointer movement. On touch devices, it reads scrolling. It starts only when the user interacts and settles again when movement stops. ### 2 · Estimate the target The engine compares the user's movement with the visible intent targets. Targets outside the viewport are excluded from active calculations. ### 3 · Run your action When one target becomes likely enough, its `onIntent` callback runs once. It can run again after the user moves away and later approaches the target again. ### Desktop and mobile - Desktop prediction follows pointer movement in two dimensions. - Mobile prediction follows vertical scroll movement. The first scroll activates it. - Hidden and off-screen targets are ignored, including elements hidden by responsive CSS. ### The physics idea Imagine each target as a small gravity well. Movement creates **kinetic energy**, while distance from a target creates **potential energy**. A fast pointer or scroll still has motion left; a slow movement close to a target looks like it is settling there. The engine uses a thermodynamics-style rule that treats these lower-energy destinations as more likely. Browser movement data contains tiny jumps and mistakes. A **Kalman filter** smooths that noise before the physics calculation. In simple terms, it helps the engine separate deliberate movement from shaky measurements. This all stays inside the library; applications only receive `onIntent`. > [Read the research paper](https://intentlink.dev/paper). This is a placeholder link until the ACM publication is available. --- ## IntentProvider Mount `IntentProvider` once near the root of your application. It runs the shared intent engine for `IntentLink` and `useIntentTarget`. ### Usage ```tsx // app/layout.tsx import { IntentProvider } from "intent-link" export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children} ) } ``` ### Notes - Use one provider for the application. Do not nest providers. - It only needs `children`; there is nothing else to configure. - It already includes its client boundary, so it can be imported by a Next.js server layout. --- ## IntentLink Use `IntentLink` wherever you would normally use Next.js `Link`. Add `onIntent` for work that should begin before the click. ### Props | Prop | Type | Default | Description | | --- | --- | --- | --- | | `href` | `string | UrlObject` | — | Required. Same as Next.js Link. | | `importance` | `"high" | "medium" | "low"` | `"medium"` | How readily the action may start. | | `cost` | `"high" | "medium" | "low"` | `"low"` | How cautious the prediction should be. | | `onIntent` | `() => void` | — | Called once when the user is likely to choose this link. | | `...rest` | — | — | Any Next.js Link or anchor prop, including className, style, and ref. | > `IntentLink` disables Next.js automatic prefetching. If you want predictive prefetching, call `router.prefetch()` inside `onIntent`. ### onIntent The callback takes no arguments and returns nothing. It runs once per approach, then rearms after the user moves away. ```ts onIntent?: () => void ``` ### Importance and cost These settings are optional. Most applications should keep the defaults. - `importance` controls how readily the action may start. Default: `medium`. - `cost` controls how cautious the prediction should be. Default: `low`. - Only change them after testing the real action on both desktop and mobile. ### Example ```tsx "use client" import { IntentLink } from "intent-link" import { useRouter } from "next/navigation" export function ProductCard() { const router = useRouter() const href = "/checkout" return ( router.prefetch(href)} > Checkout ) } ``` --- ## useIntentTarget `useIntentTarget` adds intent detection to a button, card, or other HTML element. It returns a ref; attach that ref to the element you want to observe. ### Basic usage Use the hook inside a client component and attach the returned ref to one HTML element. ```tsx "use client" import { useIntentTarget } from "intent-link" export function PreviewButton() { const intentRef = useIntentTarget({ onIntent: () => preloadProductPreview(), }) return } ``` ### Options - `onIntent`: the action to run when the element becomes the likely target. - `importance`: optional `high`, `medium`, or `low`. Default: `medium`. - `cost`: optional `high`, `medium`, or `low`. Default: `low`. ### Third-party components Attach the ref directly when the component forwards its ref to an HTML element. ```tsx const intentRef = useIntentTarget({ onIntent: () => prepareCarousel(), }) return ``` If it does not forward a ref, wrap it in a native element and attach the ref to the wrapper. ```tsx const intentRef = useIntentTarget({ onIntent: () => prepareCarousel(), }) return (
) ``` --- ## Custom Intent Components If your application has many targets of the same kind, wrap `useIntentTarget` once and reuse the resulting component. ### Reusable IntentButton This component accepts normal button props together with `onIntent`, `importance`, and `cost`. ```tsx "use client"; import type { ButtonHTMLAttributes } from "react"; import { useIntentTarget, type UseIntentTargetOptions, } from "intent-link"; type IntentButtonProps = ButtonHTMLAttributes & UseIntentTargetOptions; export function IntentButton({ onIntent, importance, cost, children, ...buttonProps }: IntentButtonProps) { const intentRef = useIntentTarget({ onIntent, importance, cost, }); return ( ); } ``` #### Usage ```tsx Preview ``` ### Reusable IntentArticle The same pattern works for product cards and other semantic containers. ```tsx "use client"; import type { HTMLAttributes } from "react"; import { useIntentTarget, type UseIntentTargetOptions, } from "intent-link"; type IntentArticleProps = HTMLAttributes & UseIntentTargetOptions; export function IntentArticle({ onIntent, importance, cost, children, ...articleProps }: IntentArticleProps) { const intentRef = useIntentTarget({ onIntent, importance, cost, }); return (
{children}
); } ``` #### Usage ```tsx preloadProduct(productId)} > ``` --- ## Examples `onIntent` can start any safe, repeatable preparation work. Keep irreversible actions—such as purchases, messages, and form submissions—behind a real click. ### Prefetch a route The common Next.js use case. ```tsx router.prefetch(href)}> {label} ``` ### Warm data Ask your data library to cache information the next screen will need. ```tsx queryClient.prefetchQuery({ queryKey: ["product", id] })} > {name} ``` ### Preload an image Begin loading a large asset before navigation. ```tsx { const image = new Image() image.src = heroImageFor(id) }} > {name} ``` ### Prepare a component Use `useIntentTarget` when the target is not a link. ```tsx const intentRef = useIntentTarget({ onIntent: () => { void import("./product-preview") }, }) return ``` --- ## Troubleshooting ### onIntent never fires - Is `IntentProvider` mounted above these links? - Does the target have an `onIntent` callback? Targets without one are not registered. - Move the pointer on desktop or scroll on mobile. The engine sleeps until the first interaction. - Make sure the target is visible and has a real width and height. - Temporarily try `importance="high"` and `cost="low"` to confirm the integration, then restore the defaults. ### A route prefetches without onIntent Check for another Next.js `Link` pointing to the same route, including links hidden by responsive styles. Also check application code that calls `router.prefetch()` directly. ### A custom component does not work The component must forward the ref to a real HTML element. If it does not, attach the intent ref to a native wrapper such as a `div`. ### Testing on mobile Use a real touch device or enable touch emulation in browser developer tools, then scroll. Merely narrowing a desktop browser window does not necessarily enable mobile behavior. ### Development and production Test important behavior in a production build too. React development checks and framework tooling can make callbacks and network requests look different from production. --- ## Changelog ### 1.0.13 (current) - Added `useIntentTarget` for buttons, cards, and third-party components. - Only visible targets participate in active prediction work. - `onIntent` is a simple void callback; internal prediction state is no longer public API. - Improved registration, rendering behavior, and high-density page performance. See the complete [version history](https://www.npmjs.com/package/intent-link?activeTab=versions) on npm. ## Project links Repository: https://github.com/Intent-Link/intent-link-npm npm: https://www.npmjs.com/package/intent-link Research paper: https://intentlink.dev/paper (placeholder until ACM publication)