Skip to Content

Suggestion in Text Input

Learn how to request completions as someone types, display them as inline ghost text, and let the user accept or reject them.

Suggestion playground

Start typing, then pause briefly to see a suggestion.


Start writing...

Press Tab to accept or Escape to dismiss.

Steps to build

1. Create the editor state and refs

Create one editor state and pass the same instance to both useSuggestion and Theodore. Keep refs for the editable element and Theodore’s imperative API.

import { useRef } from 'react'; import { Theodore, type TheodoreHandle, useEditorState } from 'theodore-js'; import 'theodore-js/style.css'; function SuggestionInTextInput() { const editorState = useEditorState(); const editorRef = useRef<HTMLDivElement>(null); const theodoreRef = useRef<TheodoreHandle>(null); return ( <Theodore ref={editorRef} theodoreRef={theodoreRef} editorState={editorState} placeholder="Start writing..." maxLines={4} /> ); }

2. Generate and display suggestions

Your application needs a way to generate suggestions, but Theodore does not care how you generate them. They can come from local logic, an AI model, or a remote service. Theodore only needs the resulting string passed to its suggestion prop.

Remote requests may need to be debounced or aborted when the user edits the content, moves the cursor, accepts or rejects a suggestion, or leaves the page. You can implement that lifecycle yourself or use the optional useSuggestion hook. The hook subscribes to content and selection changes, debounces requests, aborts pending requests when they are no longer relevant, ignores responses whose input or cursor position is stale, and keeps its suggestion state synchronized when a suggestion is accepted or rejected.

import { useRef } from 'react'; import { Theodore, type SuggestionRequest, type TheodoreHandle, useEditorState, useSuggestion, } from 'theodore-js'; function SuggestionInTextInput() { const editorState = useEditorState(); const editorRef = useRef<HTMLDivElement>(null); const theodoreRef = useRef<TheodoreHandle>(null); const { acceptSuggestion, rejectActiveSuggestion, suggestion } = useSuggestion({ debounceMs: 700, editorState, requestSuggestion, theodoreRef, }); return ( <Theodore ref={editorRef} theodoreRef={theodoreRef} editorState={editorState} suggestion={suggestion} placeholder="Start writing..." maxLines={4} /> ); } const suggestions = [ ' lorem ipsum', ' dolor sit amet', ' consectetur adipiscing elit', ' sed do eiusmod tempor', ]; function requestSuggestion({ input, signal }: SuggestionRequest) { if (signal.aborted) return Promise.resolve(null); const suggestion = suggestions[input.length % suggestions.length]; return Promise.resolve(suggestion); }

Provide useSuggestion with a requestSuggestion function that returns the suggested continuation or null.

The suggestion remains separate from the editor’s plain text until it is accepted. Clicking or tapping the ghost text accepts it. Theodore’s default hint displays the expected shortcut, while the application decides which keyboard events accept or reject the suggestion.

3. Add explicit keyboard controls

This example accepts the suggestion with Tab on desktop or Enter on mobile, and rejects it with Escape. When using useSuggestion, call the hook’s functions so its state stays synchronized with the suggestion rendered by Theodore. With a custom implementation, call the corresponding Theodore ref method and clear your own suggestion state.

useEffect(() => { const editor = editorRef.current; const handleKeyDown = (event: KeyboardEvent) => { if (suggestion == null) return; if (event.key === 'Tab' || (isMobileDevice && event.key === 'Enter')) { event.preventDefault(); event.stopImmediatePropagation(); acceptSuggestion(editorState.tree); return; } if (event.key !== 'Escape') return; event.preventDefault(); event.stopImmediatePropagation(); rejectActiveSuggestion(); }; editor?.addEventListener('keydown', handleKeyDown); return () => editor?.removeEventListener('keydown', handleKeyDown); }, [ acceptSuggestion, editorState.tree, isMobileDevice, rejectActiveSuggestion, suggestion, ]);

See the complete source code  on GitHub.

Last updated on