ForeFill
Inline ghost-text autocomplete for React textareas, inputs, and contenteditable surfaces.
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 |
npm install forefillReact 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.
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.
| 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:
weekand 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:
Thanksand 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.
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..."
/>Use the as prop to choose where autosuggest runs.
Best for multiline notes.
<ForeFill
as="textarea"
rows={3}
suggestions={suggestions}
/>Best for single-line fields.
<ForeFill
as="input"
inputType="text"
suggestions={suggestions}
/>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.
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}meansDashows no hint, whileDaican show one.- This setting works with both static and async suggestions.
| 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 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.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;
}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 supportssubstringandstartsWithonly. 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.fuzzyis still available through the exporteduseSuggestionshook 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; withsubstringmatches that wrap across multiple lines in atextarea, alignment is approximate.
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@gaccepts tojohn@gmail.com.$taccepts to$total.Happyaccepts toHappy 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.
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.
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. |
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"
/>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} />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!']}
/>
);
}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, andmaxLengthattributes apply to thetextareaandinputsurfaces. Thecontenteditablesurface is not a native form control, so it won't submit a value — use a hidden input mirror if you need that.
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 />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 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+ArrowRightthat many times. Tapping the last word accepts everything. (Gated bypartialAccept; start-anchored hints only — a prefixedsubstringhint 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>
)}
/>Use this when you want the editable surface without ghost hints.
<ForeFill
suggestions={suggestions}
disableInlineFill
/>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>
</>
);
}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}
/><ForeFill variant="outline" suggestions={suggestions} />
<ForeFill variant="filled" suggestions={suggestions} />
<ForeFill variant="underline" suggestions={suggestions} /><ForeFill size="sm" suggestions={suggestions} />
<ForeFill size="md" suggestions={suggestions} />
<ForeFill size="lg" suggestions={suggestions} />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 |
Every public prop below includes its use and a short example.
| 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} /> |
| 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" /> |
| 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} /> |
| 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" /> |
| 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 /> |
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'] } |
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>;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) |
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 forefillimport { 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 elementcreateForeFill 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,
});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');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.jsonjsdelivr/unpkgfields point atdist/vanilla.jsso the package page features a directly-usable JS file (not only the CSS URL). The React entry remains atdist/index.jsfor bundler-based React consumers.
- The same
forefill/styles.cssstylesheet 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.
Run the demo:
npm run devBuild the package:
npm run buildPreview the publish contents:
npm run pack:dryThe 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.cssnpm 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.
| 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. |
- Confirm
author,repository, andhomepageinpackage.json. - Log in once with
npm login, then confirm withnpm whoami. - Run
npm run release:check. - Optional: run
npm packand install the generated.tgzin a separate React app. - 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.