npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

use-prediction

v1.0.0

Published

Inline AI suggestions (ghost text) for existing React input and textarea fields

Readme

use-prediction

Inline AI suggestions for your existing React <input> and <textarea> fields. The user types, a suggestion appears as gray "ghost" text at the caret, and Tab inserts it.

  • Any provider. You give the hook an async get function. The hook does not call an AI provider itself.
  • Any UI kit. The hook needs only the native element through a ref. It works with controlled fields, form libraries, and component libraries.
  • Native editing. Accepted text goes through the browser's own insertion, so React onChange runs and Undo works.

React 18 and React 19 are supported.

Installation

npm install use-prediction

Usage

import { usePrediction } from "use-prediction"

export const Message = () => {
  const prediction = usePrediction({
    get: async ({ before, after, signal }) => {
      const response = await fetch("/api/predict", {
        method: "POST",
        body: JSON.stringify({ before, after }),
        signal,
      })
      const { suggestion } = await response.json()
      return suggestion
    },
  })

  return <textarea ref={prediction.ref} />
}

Your server sends the text to the model and returns the suggestion. Keep the API key on the server. For a complete server with the OpenAI SDK, see examples/server.ts.

How it behaves

| Action | Result | | --- | --- | | The user stops typing for debounce ms | The hook calls get with the text before and after the caret. A loading indicator appears at the caret until the response arrives. | | The user types the first characters of the suggestion | The suggestion gets shorter. There is no new request. | | The user keeps typing at the caret while a request is in progress | The hook waits for the response and shows the part that the user did not type yet. | | Cmd+→ (macOS) or Ctrl+→ | Inserts the next word of the suggestion. The rest stays visible. | | The user types other text, deletes text, or moves the caret | The suggestion disappears, and the hook aborts the request. | | The value changes without typing, for example a form reset | The suggestion disappears. | | Tab | Inserts the suggestion. When there is no suggestion, Tab moves focus as usual. | | Escape | Hides the suggestion. While a suggestion is visible, Escape does not close a dialog that contains the field. | | Blur, IME composition | Hides the suggestion. |

The hook shows a response only if the text and the caret did not change since the request, or if the user only typed the first characters of the response. It aborts the signal of a request that can no longer be used.

The hook works with <textarea> and with <input> of type text, search, tel, and url. For other inputs, such as password or email, it does nothing.

API

const prediction = usePrediction({
  // Required. Returns the text to insert at the caret, or "".
  get: ({ text, before, after, signal }) => Promise<string>,
  // Delay after the last keystroke, in ms. Default: 300.
  debounce: 300,
  // Class name of the ghost text. Default: the field's text color at 45% opacity.
  className: "my-ghost",
  // Shows a loading indicator at the caret while get runs. Default: true.
  loading: true,
  // Class name of the loading indicator. Default: a thin shimmering line under the text.
  loadingClassName: "my-loading",
  // Called when get fails. The field keeps working without a suggestion.
  // Default: console.error.
  onError: (error) => {},
})

prediction.ref // Ref callback for the native <input> or <textarea>.
prediction.status // "idle" | "loading" | "ready"
prediction.suggestion // The visible suggestion, or "".
prediction.accept() // Inserts the suggestion. Returns false when there is none.
prediction.dismiss() // Hides the suggestion and cancels the request.

Use status, suggestion, and accept to build your own hints, for example a "Tab" badge or an accept button on touch devices. A button must keep the focus in the field. Otherwise, the field loses focus and the suggestion disappears before the click:

{prediction.suggestion && (
  <button
    type="button"
    onPointerDown={(event) => event.preventDefault()}
    onClick={prediction.accept}
  >
    Accept
  </button>
)}

Loading indicator

While get runs, the hook shows a loading indicator: a thin line under the text that starts at the caret and runs forward, with a moving highlight. The line uses the text color of the field. It does not move or cover the text of the field, also in the middle of a line.

To use your own indicator, give a class name in loadingClassName. The class replaces the built-in size, color, and animation. The element keeps only position: absolute in a zero-size box at the caret on the text baseline. Use bottom to lift it above the baseline and inset-inline-start to move it away from the caret, so it also works in right-to-left fields:

.my-loading {
  bottom: 0.05em;
  inset-inline-start: 0.3em;
  width: 0.5em;
  height: 0.5em;
  border-radius: 50%;
  background: currentcolor;
  animation: my-pulse 1s ease-in-out infinite alternate;
}

@keyframes my-pulse {
  from {
    opacity: 0.25;
  }
}

The built-in check for reduced motion does not apply to your class. Add your own rule:

@media (prefers-reduced-motion: reduce) {
  .my-loading {
    animation: none;
  }
}

The zero-size box has data-use-prediction-loading="end" or data-use-prediction-loading="middle": the second one when the line has text after the caret. Use it to give your indicator a smaller look there, where it must not cover that text:

[data-use-prediction-loading="middle"] > .my-loading {
  bottom: -0.4em;
  inset-inline-start: 0;
  width: 0.3em;
  height: 0.3em;
}

To show no indicator, set loading: false. You can show your own status outside the field with prediction.status.

The built-in animation does not add a stylesheet to the page. When the user prefers reduced motion, the highlight does not move.

Use with a UI kit or a form library

Give prediction.ref to the native element. If the component already uses a ref, call both refs from one callback:

// React Hook Form
const { ref, ...field } = register("summary")

<textarea
  {...field}
  ref={(node) => {
    ref(node)
    prediction.ref(node)
  }}
/>
// MUI: the ref must point to the native element, not to the root <div>.
<TextField
  multiline
  slotProps={{ htmlInput: { ref: prediction.ref } }}
/>
// form-please: a custom control
const PredictedTextarea = ({ value, setValue, blur, input, props }) => {
  const prediction = usePrediction({ get: props.predict })

  return (
    <textarea
      id={input.id}
      name={input.name}
      value={value ?? ""}
      onChange={(event) => setValue(event.currentTarget.value)}
      onBlur={blur}
      ref={(node) => {
        input.ref(node)
        prediction.ref(node)
      }}
    />
  )
}

An inline callback like the ones above is safe. React gives a new callback on each render, and the hook keeps the same field attached.

Limits

  • The ghost text is an overlay in document.body, or in the open <dialog> or popover that contains the field. When a scroll container cuts off part of the field, the overlay is not cut off.
  • When the caret is in the middle of the text, the field hides its own text while the suggestion is visible, and the overlay shows the full text.
  • In Safari, one Undo can remove the accepted suggestion together with the text typed just before it.
  • When execCommand("insertText") is not available, the hook sets the value and sends an input event. React still sees the change, but Undo does not include it.

Migration from 0.0.x

  • get(value, controller) is now get({ text, before, after, signal }).
  • The color and style options are removed. Use className.
  • ref is a callback ref. You can merge it with other refs.

Development

npm install
npm run test:react19:install  # React 19 copy for the test matrix
npm test                      # unit tests on React 18 and 19
npm run verify                # types, unit tests, Biome, Knip, package build and checks
npm run test:e2e              # Chromium, Firefox, WebKit on React 18 and 19
npm run dev                   # stand; put LLM_* variables in .env.local
npm run bench                 # comparison with and without suggestions