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

forefill

v0.2.0

Published

ForeFill — inline ghost-text autocomplete for React and vanilla JS textareas, inputs, and contenteditable surfaces.

Readme


Overview

ForeFill (forefill) is a small React component that shows a ghost suggestion directly inside a textarea, input, or contenteditable surface — the user keeps typing and the rest fills in ahead of the caret.

It is designed for forms where users should keep typing naturally, then accept a suggestion with Tab or Enter.

| Capability | Included | | --- | --- | | Inline ghost hint | Yes | | Textarea, input, and contenteditable surfaces | Yes | | Prefix matching | Yes | | Substring matching | Yes | | Custom trigger suggestions | Yes | | Existing-value completion | Yes | | Smooth typed-text repositioning | Yes | | Arrow-key cycling through matching hints | Yes | | Preserves typed casing on accept | Yes | | Async suggestion source | Yes | | Validation, loading & status states | Yes | | CSS variable theming | Yes | | Automatic dark mode | Yes | | Live-region screen-reader support | Yes | | Third-party UI components | No |

Install

npm install forefill

React is an optional peer dependency. Import the component and stylesheet:

import { ForeFill } from 'forefill';
import 'forefill/styles.css';

For non-React projects, use the framework-agnostic forefill/vanilla entry (see Vanilla / CDN below) — it has no React import.

Quick Start

import { ForeFill } from 'forefill';
import 'forefill/styles.css';

const replies = [
  'Thanks so much for reaching out!',
  'Happy to help — let me take a look.',
  'Let me know if you have any questions.',
  'Looking forward to hearing from you.',
];

export function ReplyBox() {
  return (
    <ForeFill
      as="textarea"
      suggestions={replies}
      placeholder="Write a reply…"
      onCommit={(value) => console.log(value)}
    />
  );
}

That is enough for a working uncontrolled field. Add value and onChange when you want a controlled React form field.

How It Behaves

| Situation | Behavior | | --- | --- | | User types at the end of the value | ForeFill finds the best matching suggestion and renders the unmatched text as an inline ghost. | | User types in the middle or selects text | The ghost is suppressed so the overlay does not appear in the wrong position. | | Multiple suggestions match | ArrowDown and ArrowUp cycle through inline candidates when enableArrowNavigation is enabled. | | Tab is pressed with a visible hint | The hint is accepted and onAccept then onCommit can fire. | | Enter is pressed with a visible hint | Accepts the hint by default; set acceptOnEnter={false} when Enter should commit typed text only. | | Escape is pressed with a visible hint | The hint is dismissed and stays hidden until the value changes. | | A trigger suggestion is active | Trigger completions take precedence over normal static or async suggestions. | | asyncSuggestions is loading | The ghost is hidden and the loading bar/aria-busy state is shown. | | Accepted value would exceed maxLength | The completion is suppressed for native input and textarea surfaces. |

If the user types:

week

and the best suggestion is:

Hope you have a great rest of your week!

the component keeps Hope you have a great rest of your visible, shifts the typed week into the matching position, and shows the trailing ! as ghost text.

If more than one suggestion matches, press ArrowDown or ArrowUp to preview the next matching hint inline.

If the user types:

Thanks

and accepts:

Thanks so much for reaching out!

The accepted value preserves the user's typed casing. It does not force Thanks back to a differently-cased suggestion.

If the field already has a value, ForeFill still suggests against the newly typed segment. For example, a field that starts with:

Happy to help - let me take a look.

can still complete a newly appended Thanks into:

Happy to help - let me take a look. Thanks so much for reaching out!

In substring mode, the newly typed segment can also match the middle of the next suggestion. After accepting Thanks so much for reaching out!, typing if you can show Let me know on the left and have any questions. on the right, then accept into:

Thanks so much for reaching out! Let me know if you have any questions.

With matchWholeValue (on by default), the saved text is also matched as a whole, so when the field's entire value is contained in a suggestion it renders the full left+right ghost — a field holding have a great completes to Hope you have a great rest of your week!. Set matchWholeValue={false} to skip that whole-field pass. Active-segment matching, async suggestions, and trigger suggestions are unaffected either way.

Copy-Paste Recipes

Reply textarea:

<ForeFill
  as="textarea"
  suggestions={replies}
  placeholder="Write a reply..."
  showHelper="idle"
/>

Email/domain input:

<ForeFill
  as="input"
  inputType="email"
  triggerSuggestions={[
    { trigger: '@', suggestions: ['gmail.com', 'outlook.com'] },
  ]}
  placeholder="alex@g"
/>

Async suggestions:

<ForeFill
  debounceMs={250}
  asyncSuggestions={async (query, { signal }) => {
    const res = await fetch('/api/suggestions?q=' + encodeURIComponent(query), {
      signal,
    });
    return res.json();
  }}
/>

Controlled form field:

const [value, setValue] = useState('');

<ForeFill
  value={value}
  onChange={setValue}
  name="reply"
  suggestions={replies}
/>

Plain-text contenteditable:

<ForeFill
  as="contenteditable"
  suggestions={notes}
  placeholder="Draft a note..."
/>

Surface Modes

Use the as prop to choose where autosuggest runs.

Textarea

Best for multiline notes.

<ForeFill
  as="textarea"
  rows={3}
  suggestions={suggestions}
/>

Input

Best for single-line fields.

<ForeFill
  as="input"
  inputType="text"
  suggestions={suggestions}
/>

Contenteditable

Best when you need a contenteditable surface. The autosuggest value is plain text, which keeps matching, committing, and async suggestions predictable.

<ForeFill
  as="contenteditable"
  suggestions={suggestions}
/>

For full rich-text editors such as TipTap, Slate, Lexical, or Quill, use the exported useSuggestions hook with your editor's own input/caret APIs.

Activation Length

Use minQueryLength to control how many characters must be typed before autosuggest starts.

Default:

<ForeFill
  suggestions={suggestions}
  minQueryLength={1}
/>

Require at least 3 characters:

<ForeFill
  suggestions={suggestions}
  minQueryLength={3}
/>

Notes:

  • The count uses the trimmed typed value.
  • minQueryLength={3} means Da shows no hint, while Dai can show one.
  • This setting works with both static and async suggestions.

Accept And Dismiss

| User action | Result | | --- | --- | | Tab | Accept the visible ghost hint | | Enter | Accept the visible ghost hint (see acceptOnEnter) | | Ctrl/Cmd + ArrowRight | Accept one word of the hint (see partialAccept) | | ArrowDown | Preview the next matching hint | | ArrowUp | Preview the previous matching hint | | Shift+Enter | Insert a new line | | Escape | Hide the current hint and keep the typed text | | Tap the accept chip / a ghost word (touch) | Accept the whole hint, or accept up to a word (see Touch Support) | | Click away | Hide the hint and keep the typed text |

Arrow Navigation

Arrow navigation is enabled by default. It only takes over when there is a visible hint and more than one matching suggestion.

<ForeFill
  suggestions={suggestions}
  enableArrowNavigation={true}
/>

Turn it off:

<ForeFill
  suggestions={suggestions}
  enableArrowNavigation={false}
/>

Example: if the user types Thanks, these suggestions can be cycled inline:

Thanks so much for reaching out!
Thanks for the quick turnaround.
Thanks again for your patience.

Inline Helper

The helper appears inside the editable surface after the ghost hint:

Thanks so much for reaching out! | [Up] or [Down] changes hint. [Tab] or [Enter] accepts the hint. [Esc] hides it.

The Up and Down keycaps appear only when enableArrowNavigation is enabled and more than one hint matches.

Hide it completely:

<ForeFill
  suggestions={suggestions}
  showHelper={false}
/>

Show it immediately when a hint appears:

<ForeFill
  suggestions={suggestions}
  showHelper={true}
/>

Show it only after the user pauses typing:

<ForeFill
  suggestions={suggestions}
  showHelper="idle"
  helperIdleMs={900}
  enableArrowNavigation={true}
/>

Customize the helper content:

<ForeFill
  suggestions={suggestions}
  showHelper={true}
  helperText={
    <>
      <kbd>Tab</kbd> accepts. <kbd>Esc</kbd> hides.
    </>
  }
/>

Style the helper:

<ForeFill
  suggestions={suggestions}
  showHelper={true}
  helperClassName="reply-helper"
/>
.reply-helper {
  color: #475569;
  --ff-helper-font-size: 0.72em;
  --ff-helper-gap: 0.25em;
}

.reply-helper .ff-key {
  min-width: 1.9em;
  min-height: 1.3em;
  padding-inline: 0.32em;
  border-radius: 3px;
}

.reply-helper .ff-key {
  border-color: #94a3b8;
}

Match Modes

Use substring when the typed text can appear anywhere inside a suggestion.

<ForeFill
  suggestions={suggestions}
  matchMode="substring"
/>

Use startsWith when suggestions should only match from the beginning.

<ForeFill
  suggestions={suggestions}
  matchMode="startsWith"
/>

Note on fuzzy. The component supports substring and startsWith only. A fuzzy match has no single contiguous span for the typed text to slot into, so it can't be rendered as an inline ghost. fuzzy is still available through the exported useSuggestions hook for building list-style autocompletes.

Note on the caret. The inline ghost is only shown while the caret sits at the end of the value — the point where the suggestion would actually be appended. Click or arrow into the middle of the text and the hint steps aside until you return to the end. With matchMode="startsWith" the typed text and the ghost are always pixel-aligned; with substring matches that wrap across multiple lines in a textarea, alignment is approximate.

Trigger Suggestions

Use triggerSuggestions when a specific symbol or word should open its own completion set. Triggers use startsWith matching and take precedence over normal suggestions while active.

<ForeFill
  as="input"
  triggerSuggestions={[
    { trigger: '@', suggestions: ['gmail.com', 'yahoo.com', 'outlook.com'] },
    { trigger: '$', suggestions: ['total', 'subtotal', 'tax'] },
    { trigger: '&', suggestions: [' shipping', ' handling'] },
    { trigger: 'Happy', suggestions: [' Birthday!'] },
  ]}
/>

Examples:

  • john@g accepts to [email protected].
  • $t accepts to $total.
  • Happy accepts to Happy Birthday! because the suggestion starts with a leading space.
type ForeFillTriggerSuggestion = {
  trigger: string;
  suggestions: string[];
  minQueryLength?: number;
  caseSensitive?: boolean;
};

Symbol triggers such as @, $, or & can activate anywhere in the active typed segment. Word triggers such as Happy activate at the start of the segment or after whitespace/punctuation.

Async Suggestions

Use asyncSuggestions when suggestions come from an API. debounceMs delays the request while the user is typing, and the second argument includes an AbortSignal for cancelling stale requests.

<ForeFill
  minQueryLength={2}
  debounceMs={250}
  asyncSuggestions={async (query, { signal }) => {
    const res = await fetch(`/api/suggestions?q=${encodeURIComponent(query)}`, {
      signal,
    });
    return res.json();
  }}
/>

When the query changes, ForeFill aborts the previous request and clears stale matches. If a request fails, the component stops loading and shows no old async hint.

Validation And Status

Drive the error, success, and loading treatments declaratively.

<ForeFill status="error" />

<ForeFill status="success" />

status accepts 'idle' | 'loading' | 'success' | 'error'. status="loading" shows an indeterminate progress bar — this is also inferred automatically while an asyncSuggestions request is in flight, so async users get the bar for free.

| Prop | Effect | | --- | --- | | status="error" | Error border + ring, aria-invalid. | | status="success" | Success border + ring. | | status="loading" | Indeterminate progress bar, aria-busy. |

Accessibility Labels

Use ariaLabel for unlabeled fields, or ariaLabelledBy when a visible label already exists. Use ariaDescribedBy for external helper or error text; ForeFill merges it with the internal keyboard helper description when the helper is visible.

<label id="reply-label">Reply</label>
<p id="reply-help">Use Tab to accept the inline suggestion.</p>

<ForeFill
  ariaLabelledBy="reply-label"
  ariaDescribedBy="reply-help"
  suggestions={suggestions}
  showHelper="idle"
/>

Dark Mode

The component follows the operating system prefers-color-scheme by default. Force a scheme with the theme prop, or override tokens under your own data-theme selector.

<ForeFill theme="dark" suggestions={suggestions} />
<ForeFill theme="light" suggestions={suggestions} />

Controlled Value

import { useState } from 'react';
import { ForeFill } from 'forefill';

export function ControlledExample() {
  const [value, setValue] = useState('');

  return (
    <ForeFill
      value={value}
      onChange={setValue}
      suggestions={['Thanks so much for reaching out!']}
    />
  );
}

Form Integration

ForeFill forwards the standard form attributes to the underlying input/textarea, so it participates in native <form> submission and validation just like a plain field.

<form onSubmit={handleSubmit}>
  <label htmlFor="reply">Reply</label>
  <ForeFill
    as="input"
    id="reply"
    name="reply"
    required
    maxLength={280}
    suggestions={suggestions}
  />
  <button type="submit">Send</button>
</form>

Supported passthrough props: name, id, required, readOnly, maxLength, and autoFocus. The component's own value and handlers always take precedence, so these can't break its behavior. readOnly also suppresses the inline ghost. (Browser autofill and spellcheck are disabled internally so they never compete with the inline ghost.)

The name, required, and maxLength attributes apply to the textarea and input surfaces. The contenteditable surface is not a native form control, so it won't submit a value — use a hidden input mirror if you need that.

Accept, Dismiss & Commit Callbacks

onAccept tells you the user took a suggestion (and which one), separate from onCommit — which also fires when they commit their own typed text. onDismiss fires when a visible hint is escaped.

<ForeFill
  suggestions={suggestions}
  onAccept={(value, suggestion) => track('accepted', suggestion)}
  onCommit={(value) => save(value)}
  onDismiss={() => track('dismissed')}
/>

Tune how the field finalizes:

{/* Enter no longer accepts the hint — only Tab does. Enter commits typed text. */}
<ForeFill suggestions={suggestions} acceptOnEnter={false} />

{/* Commit the current value whenever focus leaves the field. */}
<ForeFill suggestions={suggestions} commitOnBlur />

Word-by-Word Accept

Press Ctrl / Cmd + ArrowRight to accept one word of the hint at a time instead of the whole thing, then keep typing or accept more. Enabled by default; turn it off with partialAccept={false}.

<ForeFill suggestions={suggestions} partialAccept />

Word-by-word accept applies to start-anchored hints (where the typed text begins the suggestion). For substring matches that have text before the typed portion, Ctrl/Cmd + ArrowRight falls through to the browser's normal word navigation.

Touch Support

Touch devices have no Tab key and no Ctrl/Cmd + ArrowRight, so by default ForeFill shows tappable touch controls whenever a coarse pointer is detected:

  • An accept chip floating over the field — tap it to accept the whole hint.
  • Tappable ghost words — tap a gray word to accept up to (and including) it, the same as pressing Ctrl/Cmd + ArrowRight that many times. Tapping the last word accepts everything. (Gated by partialAccept; start-anchored hints only — a prefixed substring hint accepts in full on tap.)
// 'auto' (default) shows the controls only on coarse pointers.
<ForeFill suggestions={suggestions} touchAccept="auto" />

// Force them on (e.g. for a hybrid touch + mouse device), or off.
<ForeFill suggestions={suggestions} touchAccept />
<ForeFill suggestions={suggestions} touchAccept={false} />

The controls fire on pointerdown and keep focus on the editor, so the soft keyboard stays up and you can keep typing after accepting. The helper text and the screen-reader announcement switch to touch wording automatically.

Bring your own control with renderTouchAccept. The returned node must call accept() from an onPointerDown that calls preventDefault(), otherwise the editor blurs and the hint is cleared before the accept runs:

<ForeFill
  suggestions={suggestions}
  touchAccept
  renderTouchAccept={({ accept, suggestion, label }) => (
    <button
      type="button"
      aria-label={label}
      onPointerDown={(e) => {
        e.preventDefault();
        accept();
      }}
    >
      ↵ {suggestion}
    </button>
  )}
/>

Disable Inline Fill

Use this when you want the editable surface without ghost hints.

<ForeFill
  suggestions={suggestions}
  disableInlineFill
/>

Imperative Ref

import { useRef } from 'react';
import {
  ForeFill,
  type ForeFillHandle,
} from 'forefill';

export function FocusExample() {
  const ref = useRef<ForeFillHandle>(null);

  return (
    <>
      <ForeFill
        ref={ref}
        suggestions={['Thanks so much for reaching out!']}
      />
      <button type="button" onClick={() => ref.current?.focus()}>
        Focus
      </button>
    </>
  );
}

Styling

The package ships a single stylesheet:

import 'forefill/styles.css';

Customize with CSS variables:

.reply-field {
  --ff-bg: #ffffff;
  --ff-text: #0f172a;
  --ff-muted: #64748b;
  --ff-border: #cbd5e1;
  --ff-accent: #2563eb;
  --ff-success: #0e9f6e;
  --ff-danger: #e02424;
  --ff-radius: 8px;
  --ff-font-size: 14px;
  --ff-min-height: 84px;
  --ff-input-min-height: 40px;
}
<ForeFill
  className="reply-field"
  suggestions={suggestions}
/>

Visual Variants

<ForeFill variant="outline" suggestions={suggestions} />
<ForeFill variant="filled" suggestions={suggestions} />
<ForeFill variant="underline" suggestions={suggestions} />

Sizes

<ForeFill size="sm" suggestions={suggestions} />
<ForeFill size="md" suggestions={suggestions} />
<ForeFill size="lg" suggestions={suggestions} />

CSS Tokens

Every visual value is exposed as a CSS custom property. Override any of these in your own stylesheet to re-skin the component.

| Token | Purpose | | --- | --- | | --ff-bg | Field background | | --ff-text | Typed text color | | --ff-muted | Placeholder, helper, and ghost text color | | --ff-faint | Ghost prefix/suffix color | | --ff-border | Resting border color | | --ff-accent | Focus border, caret, and ring color | | --ff-success | Success state color | | --ff-danger | Error state color | | --ff-radius | Border radius | | --ff-ease | Animation easing curve | | --ff-duration | Base transition duration | | --ff-key-bg | Helper keycap background | | --ff-key-border | Helper keycap border | | --ff-key-shadow | Helper keycap shadow | | --ff-helper-font-size | Inline helper text size | | --ff-helper-gap | Space between helper pieces | | --ff-helper-margin-left | Space before the helper begins | | --ff-helper-opacity | Helper opacity | | --ff-helper-separator-margin-right | Space after the separator | | --ff-key-min-width | Keycap minimum width | | --ff-key-min-height | Keycap minimum height | | --ff-key-padding-x | Keycap horizontal padding | | --ff-key-radius | Keycap border radius | | --ff-key-font-size | Keycap text size | | --ff-key-font-weight | Keycap font weight | | --ff-touch-accept-size | Touch accept chip target size (default 34px; set to 44px for WCAG 2.5.5) | | --ff-touch-accept-glyph-size | Touch accept chip glyph (↵) text size | | --ff-font | Font family | | --ff-padding-y | Vertical padding | | --ff-padding-x | Horizontal padding | | --ff-font-size | Text size | | --ff-line-height | Text line height | | --ff-min-height | Minimum textarea/contenteditable height | | --ff-input-min-height | Input height |

Props

Every public prop below includes its use and a short example.

Suggestions And Matching

| Prop | Type / default | Use | Example | | --- | --- | --- | --- | | suggestions | string[], default [] | Static suggestion list used when no trigger completion is active. | <ForeFill suggestions={['Thanks so much!']} /> | | triggerSuggestions | ForeFillTriggerSuggestion[], default [] | Adds trigger-specific completions for symbols or words, such as email domains after @. | <ForeFill triggerSuggestions={[{ trigger: '@', suggestions: ['gmail.com'] }]} /> | | asyncSuggestions | (query: string, context?: { signal: AbortSignal }) => Promise<string[]> | Loads suggestions from an API, aborts stale requests, and takes precedence over suggestions. | <ForeFill asyncSuggestions={fetchSuggestions} debounceMs={250} /> | | matchMode | 'startsWith' \| 'substring', default 'substring' | Controls matching for normal suggestions; trigger suggestions always use starts-with matching. | <ForeFill matchMode="startsWith" /> | | matchWholeValue | boolean, default true | When the field already contains text, also matches the whole value against the static suggestions so a suggestion that contains it renders the full left+right ghost. Off skips only that whole-field pass; active-segment matching still works. Async/trigger sources are unaffected. | <ForeFill matchWholeValue={false} /> | | minQueryLength | number, default 1 | Requires a trimmed query length before suggestions activate. | <ForeFill minQueryLength={3} /> | | debounceMs | number, default 0 | Delays async requests while the user is typing. | <ForeFill asyncSuggestions={fetchSuggestions} debounceMs={300} /> | | disableInlineFill | boolean, default false | Keeps the editable field behavior but hides ghost hints. | <ForeFill disableInlineFill /> | | enableArrowNavigation | boolean, default true | Lets Up and Down cycle through multiple matching hints. | <ForeFill enableArrowNavigation={false} /> |

Surface And Value

| Prop | Type / default | Use | Example | | --- | --- | --- | --- | | as | 'textarea' \| 'input' \| 'contenteditable', default 'textarea' | Chooses the editable surface. | <ForeFill as="input" suggestions={suggestions} /> | | rows | number, default 3 | Sets visible rows for as="textarea". | <ForeFill as="textarea" rows={5} /> | | inputType | string, default 'text' | Sets the native type for as="input". | <ForeFill as="input" inputType="email" /> | | placeholder | string, default 'Type to search...' | Sets the placeholder or contenteditable empty-state text. | <ForeFill placeholder="Write a reply..." /> | | defaultValue | string, default '' | Sets the initial uncontrolled value. | <ForeFill defaultValue="Existing text. " /> | | value | string | Makes the component controlled; pair it with onChange. | <ForeFill value={value} onChange={setValue} /> | | disabled | boolean, default false | Disables input and interactive completion. | <ForeFill disabled /> | | readOnly | boolean, default false | Shows a value without allowing edits or hints. | <ForeFill readOnly value="Saved reply" /> |

Events And Accept Behavior

| Prop | Type / default | Use | Example | | --- | --- | --- | --- | | onChange | (value: string) => void | Runs on every typed value change. | <ForeFill value={value} onChange={setValue} /> | | onCommit | (value: string) => void | Runs when the user commits typed text or accepts a hint. | <ForeFill onCommit={(value) => save(value)} /> | | onAccept | (value: string, suggestion: string) => void | Runs only when a suggestion is accepted. | <ForeFill onAccept={(_value, suggestion) => track(suggestion)} /> | | onDismiss | () => void | Runs when Escape hides a visible hint. | <ForeFill onDismiss={() => track('dismissed')} /> | | acceptOnEnter | boolean, default true | When false, Enter commits typed text and Tab accepts hints. | <ForeFill acceptOnEnter={false} /> | | commitOnBlur | boolean, default false | Commits the current value when focus leaves the field. | <ForeFill commitOnBlur onCommit={save} /> | | partialAccept | boolean, default true | Lets users accept one word with Ctrl/Cmd + ArrowRight (and by tapping a ghost word on touch). | <ForeFill partialAccept={false} /> | | touchAccept | boolean \| 'auto', default 'auto' | Shows tappable touch controls (accept chip + ghost words). 'auto' = coarse pointers only; true/false force on/off. | <ForeFill touchAccept /> | | touchAcceptLabel | string, default 'Accept suggestion' | Accessible label for the accept chip. | <ForeFill touchAcceptLabel="Use suggestion" /> | | touchAcceptClassName | string | Extra classes merged onto the accept chip. | <ForeFill touchAcceptClassName="my-chip" /> | | renderTouchAccept | (api: TouchAcceptRenderProps) => ReactNode | Render your own accept control; call accept() from a preventDefault-ed onPointerDown. | See Touch Support | | active | boolean, default false | Renders the inline ghost as if focused without stealing DOM focus (avoids opening the soft keyboard on touch). For showcase/preview scenarios with an externally driven value. | <ForeFill active value={value} suggestions={suggestions} /> |

Helper, Status, And Accessibility

| Prop | Type / default | Use | Example | | --- | --- | --- | --- | | ariaLabel | string, default placeholder | Labels the field when no visible label is connected. | <ForeFill ariaLabel="Compose reply" /> | | ariaLabelledBy | string | Uses an external visible label as the accessible name. | <ForeFill ariaLabelledBy="reply-label" /> | | ariaDescribedBy | string | Merges external helper/error text with ForeFill's internal helper description. | <ForeFill ariaDescribedBy="reply-help" /> | | showHelper | boolean \| 'idle', default false | Shows the inline keyboard helper immediately, after idle, or never. | <ForeFill showHelper="idle" /> | | helperIdleMs | number, default 900 | Controls the idle delay when showHelper="idle". | <ForeFill showHelper="idle" helperIdleMs={1200} /> | | helperText | ReactNode, default built-in helper | Replaces the helper copy with custom React content. | <ForeFill showHelper helperText="Tab accepts" /> | | helperClassName | string | Styles the helper wrapper with a class. | <ForeFill showHelper helperClassName="reply-helper" /> | | status | 'idle' \| 'loading' \| 'success' \| 'error' | Drives loading, success, or error treatments; error sets aria-invalid. | <ForeFill status="error" /> |

Styling And Forms

| Prop | Type / default | Use | Example | | --- | --- | --- | --- | | className | string | Styles the root wrapper and scopes CSS variable overrides. | <ForeFill className="reply-field" /> | | editorClassName | string | Styles the editable textarea, input, or contenteditable element. | <ForeFill editorClassName="reply-editor" /> | | variant | 'outline' \| 'filled' \| 'underline', default 'outline' | Chooses the built-in border/fill treatment. | <ForeFill variant="filled" /> | | size | 'sm' \| 'md' \| 'lg', default 'md' | Adjusts font size, padding, and minimum height. | <ForeFill size="lg" /> | | theme | 'light' \| 'dark' | Forces a color scheme; omit it to follow system preference. | <ForeFill theme="dark" /> | | name | string | Forwards the native form name to input/textarea. | <ForeFill as="input" name="reply" /> | | id | string | Sets the editable element id for labels. | <ForeFill id="reply" /> | | required | boolean, default false | Marks input/textarea required and sets aria-required. | <ForeFill required /> | | maxLength | number | Sets the native maximum length for input/textarea. | <ForeFill maxLength={280} /> | | autoFocus | boolean, default false | Focuses the field on mount. | <ForeFill autoFocus /> |

Trigger Suggestion Type

type ForeFillTriggerSuggestion = {
  trigger: string;
  suggestions: string[];
  minQueryLength?: number;
  caseSensitive?: boolean;
};

| Field | Use | Example | | --- | --- | --- | | trigger | Text that activates a completion group. | { trigger: '@', suggestions: ['gmail.com'] } | | suggestions | Values offered after the trigger. Leading spaces are preserved. | { trigger: 'Happy', suggestions: [' Birthday!'] } | | minQueryLength | Optional query length required after the trigger. | { trigger: '@', minQueryLength: 1, suggestions: ['gmail.com'] } | | caseSensitive | Requires exact casing for the trigger and query. | { trigger: 'SKU', caseSensitive: true, suggestions: ['-001'] } |

Imperative Handle Type

Use ref when another control needs to move focus or selection.

type ForeFillHandle = {
  focus: () => void;
  blur: () => void;
  select: () => void;
};
const ref = useRef<ForeFillHandle>(null);

<ForeFill ref={ref} suggestions={suggestions} />;
<button type="button" onClick={() => ref.current?.focus()}>Focus</button>;

Suggestion Hook

The matching engine is also exported for custom interfaces.

import { useSuggestions } from 'forefill';

function CustomAutosuggest({ suggestions }: { suggestions: string[] }) {
  const [query, setQuery] = useState('');
  const { matches, isLoading } = useSuggestions(query, {
    suggestions,
    minQueryLength: 2,
    matchMode: 'substring',
  });

  return null;
}
type UseSuggestionsOptions = {
  suggestions?: string[];
  asyncFetcher?: (
    query: string,
    context?: { signal: AbortSignal }
  ) => Promise<string[]>;
  minQueryLength?: number;
  debounceMs?: number;
  matchMode?: 'startsWith' | 'substring' | 'fuzzy';
};

type UseSuggestionsResult = {
  matches: string[];
  isLoading: boolean;
};

| Option / result | Use | Example | | --- | --- | --- | | suggestions | Static list filtered on the client. | useSuggestions(query, { suggestions }) | | asyncFetcher | Async source with an AbortSignal; replaces static filtering. | useSuggestions(query, { asyncFetcher: fetchSuggestions }) | | minQueryLength | Returns no matches until the trimmed query is long enough. | useSuggestions(query, { minQueryLength: 2 }) | | debounceMs | Debounces async queries. | useSuggestions(query, { asyncFetcher, debounceMs: 250 }) | | matchMode | Supports startsWith, substring, and hook-only fuzzy. | useSuggestions(query, { matchMode: 'fuzzy' }) | | matches | The filtered suggestion strings. | const { matches } = useSuggestions(query, options) | | isLoading | True while an async fetch is in flight. | const { isLoading } = useSuggestions(query, options) |

Vanilla / CDN

For non-React apps (plain JS, Vue, Svelte, Solid, Alpine, htmx, vanilla TS), ForeFill ships a framework-agnostic entry at forefill/vanilla. It has no React import and works with any existing <textarea>, <input>, or [contenteditable] element. The surface is inferred from the element.

npm install forefill
import { attachForeFill } from 'forefill/vanilla';
import 'forefill/styles.css';

const ff = attachForeFill(document.querySelector<HTMLTextAreaElement>('#reply'), {
  suggestions: [
    'Thanks so much for reaching out!',
    'Happy to help — let me take a look.',
  ],
  matchMode: 'substring',
  onAccept: (value, suggestion) => track('accepted', suggestion),
  onCommit: (value) => save(value),
  onDismiss: () => track('dismissed'),
});

ff.focus();
// ...later
ff.destroy(); // removes the overlay and restores the original element

createForeFill builds the editor element for you when you don't have one:

import { createForeFill } from 'forefill/vanilla';
import 'forefill/styles.css';

const ff = createForeFill(document.querySelector('#host')!, {
  as: 'textarea', // 'textarea' | 'input' | 'contenteditable'
  suggestions: notes,
});

Vanilla API

function attachForeFill(
  editor: HTMLTextAreaElement | HTMLInputElement | HTMLDivElement,
  options: ForeFillVanillaOptions
): ForeFillInstance;

function createForeFill(
  container: HTMLElement,
  options: CreateForeFillOptions
): ForeFillInstance;

ForeFillVanillaOptions mirrors the React props, with two vanilla-shaped fields: helperText is a string (not ReactNode) and renderTouchAccept returns an HTMLElement. There is no controlled value prop — the editor element is the source of truth; use setValue() to set it programmatically.

type ForeFillInstance = {
  focus: () => void;
  blur: () => void;
  select: () => void;
  setValue: (next: string) => void;
  getValue: () => string;
  setOptions: (options: Partial<ForeFillVanillaOptions>) => void;
  destroy: () => void;
};

The matching engine is also exported for custom UIs:

import { filterMatches } from 'forefill/vanilla';
filterMatches('Thanks', suggestions, 'substring');

Via CDN (no bundler)

The package ships a self-contained vanilla build, so you can load it directly from jsDelivr/unpkg without React or a build step:

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/forefill/dist/styles.css">
<script type="module">
  import { attachForeFill } from 'https://cdn.jsdelivr.net/npm/forefill/dist/vanilla.js';
  attachForeFill(document.querySelector('#reply'), {
    suggestions: ['Thanks so much for reaching out!'],
    onCommit: (v) => console.log(v),
  });
</script>

The package.json jsdelivr/unpkg fields point at dist/vanilla.js so the package page features a directly-usable JS file (not only the CSS URL). The React entry remains at dist/index.js for bundler-based React consumers.

Vanilla notes

  • The same forefill/styles.css stylesheet themes both the React component and the vanilla entry.
  • Behavior (ghost rendering, Tab/Enter accept, Escape, ArrowDown/Up cycling, Ctrl/Cmd+ArrowRight word-by-word accept, triggers, async with abort, maxLength suppression, touch controls) mirrors the React component.
  • The vanilla core lives under src/lib/core/* and is a parallel implementation (it does not import the React component), so the two surfaces can evolve independently.

Local Development

Run the demo:

npm run dev

Build the package:

npm run build

Preview the publish contents:

npm run pack:dry

The package build emits:

dist/index.js
dist/index.cjs
dist/index.d.ts
dist/vanilla.js
dist/vanilla.cjs
dist/vanilla.d.ts
dist/**/*.d.ts
dist/styles.css

npm run pack:dry should include those library artifacts plus README.md, LICENSE, and package.json. It should not include demo assets such as ForeFill.svg, ForeFill.json, or favicon.svg.

Troubleshooting

| Symptom | Check | | --- | --- | | No ghost appears | Make sure the field is focused, the caret is at the end, the query meets minQueryLength, and disableInlineFill is not set. | | Enter accepts when it should only commit | Set acceptOnEnter={false}; Tab will still accept visible hints. | | Async results look stale | Use the signal passed to asyncSuggestions / asyncFetcher in your fetch call. ForeFill aborts old requests when the query changes. | | Screen readers do not announce your label | Use ariaLabel for unlabeled fields or ariaLabelledBy to point at a visible label ID. | | Error/helper text is not announced | Pass the external text ID with ariaDescribedBy; ForeFill merges it with its own helper description. | | contenteditable does not submit in a form | contenteditable is not a native form control. Mirror the value into a hidden input if you need native form submission. | | Hint disappears near maxLength | Accepted completions that would exceed native maxLength are intentionally suppressed. |

Publish Checklist

  1. Confirm author, repository, and homepage in package.json.
  2. Log in once with npm login, then confirm with npm whoami.
  3. Run npm run release:check.
  4. Optional: run npm pack and install the generated .tgz in a separate React app.
  5. Publish with npm run publish:npm.

For the first public publish, the package name forefill must still be available on npm. If it is not, change name to a scoped package such as @your-npm-username/forefill and publish with the same npm run publish:npm script.