Packages
@anvia/react-ui: Reference
Composable UI primitives from @anvia/react-ui.
Import from @anvia/react-ui.
Subpath entrypoints are also available:
@anvia/react-ui/chat@anvia/react-ui/completion@anvia/react-ui/attachment@anvia/react-ui/human-input@anvia/react-ui/image@anvia/react-ui/message@anvia/react-ui/selection-toolbar@anvia/react-ui/shared@anvia/react-ui/stream@anvia/react-ui/stream/styles.css@anvia/react-ui/thread-list@anvia/react-ui/styles.css
Primary namespaces
const Thread: {
Root: React.ForwardRefExoticComponent<...>;
Viewport: React.ForwardRefExoticComponent<...>;
ViewportFooter: React.ForwardRefExoticComponent<...>;
Messages: React.ForwardRefExoticComponent<...>;
Empty: React.ForwardRefExoticComponent<...>;
Status: React.ForwardRefExoticComponent<...>;
Loading: React.ForwardRefExoticComponent<...>;
Error: React.ForwardRefExoticComponent<...>;
Suggestions: React.ForwardRefExoticComponent<...>;
Suggestion: React.ForwardRefExoticComponent<...>;
ScrollToBottom: React.ForwardRefExoticComponent<...>;
};
const Composer: {
Root: React.ForwardRefExoticComponent<...>;
Quote: React.ForwardRefExoticComponent<...>;
ClearQuote: React.ForwardRefExoticComponent<...>;
Input: React.ForwardRefExoticComponent<
React.HTMLAttributes<HTMLDivElement> & {
autoResize?: boolean;
maxRows?: number;
minRows?: number;
placeholder?: string;
}
>;
TextareaInput: React.ForwardRefExoticComponent<
React.TextareaHTMLAttributes<HTMLTextAreaElement> & {
autoResize?: boolean;
maxRows?: number;
minRows?: number;
}
>;
TriggerMenu: React.ForwardRefExoticComponent<...>;
TriggerItem: React.ForwardRefExoticComponent<...>;
Attachments: React.ForwardRefExoticComponent<...>;
AttachmentInput: React.ForwardRefExoticComponent<...>;
AddAttachment: React.ForwardRefExoticComponent<...>;
AttachmentDropzone: React.ForwardRefExoticComponent<...>;
Submit: React.ForwardRefExoticComponent<...>;
Stop: React.ForwardRefExoticComponent<...>;
};
const Attachment: {
Root: React.ForwardRefExoticComponent<...>;
Name: React.ForwardRefExoticComponent<...>;
Preview: React.ForwardRefExoticComponent<...>;
Remove: React.ForwardRefExoticComponent<...>;
};
const Image: {
Root: React.ForwardRefExoticComponent<...>;
Preview: React.ForwardRefExoticComponent<...>;
Name: React.ForwardRefExoticComponent<...>;
Actions: React.ForwardRefExoticComponent<...>;
Copy: React.ForwardRefExoticComponent<...>;
Download: React.ForwardRefExoticComponent<...>;
ZoomTrigger: React.ForwardRefExoticComponent<...>;
ZoomOverlay: React.ForwardRefExoticComponent<...>;
};
const Message: {
Root: React.ForwardRefExoticComponent<...>;
Content: React.ForwardRefExoticComponent<...>;
Parts: React.ForwardRefExoticComponent<...>;
Part: React.ForwardRefExoticComponent<...>;
Text: React.ForwardRefExoticComponent<...>;
Markdown: React.ForwardRefExoticComponent<...>;
Entity: React.ForwardRefExoticComponent<MessageEntityProps & React.RefAttributes<HTMLSpanElement>>;
CodeBlock: React.ForwardRefExoticComponent<...>;
Reasoning: React.ForwardRefExoticComponent<...>;
Tool: React.ForwardRefExoticComponent<...>;
ToolName: React.ForwardRefExoticComponent<...>;
ToolInput: React.ForwardRefExoticComponent<...>;
ToolOutput: React.ForwardRefExoticComponent<...>;
ToolError: React.ForwardRefExoticComponent<...>;
ToolStatus: React.ForwardRefExoticComponent<...>;
Attachment: React.ForwardRefExoticComponent<...>;
Data: React.ForwardRefExoticComponent<...>;
Error: React.ForwardRefExoticComponent<...>;
Actions: React.ForwardRefExoticComponent<...>;
Copy: React.ForwardRefExoticComponent<...>;
Regenerate: React.ForwardRefExoticComponent<...>;
};
const Completion: {
Root: React.ForwardRefExoticComponent<...>;
Output: React.ForwardRefExoticComponent<...>;
Form: React.ForwardRefExoticComponent<...>;
Input: React.ForwardRefExoticComponent<...>;
Submit: React.ForwardRefExoticComponent<...>;
Stop: React.ForwardRefExoticComponent<...>;
};
const HumanInput: {
Panel: React.ForwardRefExoticComponent<...>;
Status: React.ForwardRefExoticComponent<...>;
Approvals: React.ForwardRefExoticComponent<...>;
Approval: React.ForwardRefExoticComponent<...>;
ApprovalReason: React.ForwardRefExoticComponent<...>;
Approve: React.ForwardRefExoticComponent<...>;
Reject: React.ForwardRefExoticComponent<...>;
Questions: React.ForwardRefExoticComponent<...>;
Question: React.ForwardRefExoticComponent<...>;
QuestionPrompt: React.ForwardRefExoticComponent<...>;
QuestionChoice: React.ForwardRefExoticComponent<...>;
QuestionTextAnswer: React.ForwardRefExoticComponent<...>;
QuestionSubmit: React.ForwardRefExoticComponent<...>;
};
const SelectionToolbar: {
Root: React.ForwardRefExoticComponent<...>;
Quote: React.ForwardRefExoticComponent<...>;
Copy: React.ForwardRefExoticComponent<...>;
};
const ThreadList: {
Root: React.ForwardRefExoticComponent<...>;
New: React.ForwardRefExoticComponent<...>;
Items: React.ForwardRefExoticComponent<...>;
Empty: React.ForwardRefExoticComponent<...>;
};
const ThreadListItem: {
Root: React.ForwardRefExoticComponent<...>;
Trigger: React.ForwardRefExoticComponent<...>;
Title: React.ForwardRefExoticComponent<...>;
Archive: React.ForwardRefExoticComponent<...>;
Unarchive: React.ForwardRefExoticComponent<...>;
Delete: React.ForwardRefExoticComponent<...>;
};
Streamed text smoothing
Message.Parts, Message.Text, and Message.Markdown accept an optional stream lifecycle:
type MessageStreamOptions = {
isStreaming: boolean;
resetKey: string | number;
flushImmediately?: boolean;
};
Smoothing is disabled by default. Keep stream present when isStreaming changes to false so
buffered text drains. Message.Parts smooths text and reasoning together and delays later opaque
parts, such as tools, until preceding text is visible. The underlying useChat controller and
UIMessage[] remain unchanged.
Message.Markdown also accepts renderEntity?: (entity: ComposerEntity) => ReactNode. Valid
entities from message.metadata.composer.entities render through Message.Entity by default.
type MessageEntityProps = React.HTMLAttributes<HTMLSpanElement> & {
entity: ComposerEntity;
};
The default span emits data-anvia-message-entity, data-entity-id, and data-trigger-id. It does
not serialize entity.data into the DOM.
StreamMarkdown
Import StreamMarkdown from @anvia/react-ui or @anvia/react-ui/stream:
type StreamMarkdownProps = React.HTMLAttributes<HTMLDivElement> & {
content: string;
live?: boolean;
components?: Components;
remarkPlugins?: ReactMarkdownOptions["remarkPlugins"];
remarkRehypeOptions?: ReactMarkdownOptions["remarkRehypeOptions"];
};
StreamMarkdown is context-free and does not pace content itself. It keeps completed top-level
Markdown blocks stable as content grows and applies a two-band reveal only to text in the final
live block. Preformatted code is excluded. Import @anvia/react-ui/stream/styles.css when using the
subpath by itself.
Providers
type ChatController<TEvent = unknown> = UseChatResult<TEvent>;
type CompletionController<TEvent = unknown> = UseCompletionResult<TEvent>;
type ChatProviderProps<TEvent = unknown> = {
controller: ChatController<TEvent>;
children?: React.ReactNode;
};
type CompletionProviderProps<TEvent = unknown> = {
controller: CompletionController<TEvent>;
children?: React.ReactNode;
};
type ThreadListRecord = {
id: string;
title?: string;
createdAt?: string | Date;
updatedAt?: string | Date;
archived?: boolean;
metadata?: unknown;
};
type ThreadListController = {
threads: ThreadListRecord[];
activeThreadId?: string;
status?: "idle" | "loading" | "error";
error?: unknown;
createThread(): Promise<void> | void;
switchThread(threadId: string): Promise<void> | void;
archiveThread?(threadId: string): Promise<void> | void;
unarchiveThread?(threadId: string): Promise<void> | void;
deleteThread?(threadId: string): Promise<void> | void;
};
type ThreadListProviderProps = {
controller: ThreadListController;
children?: React.ReactNode;
};
function ChatProvider<TEvent = unknown>(props: ChatProviderProps<TEvent>): React.ReactElement;
function CompletionProvider<TEvent = unknown>(
props: CompletionProviderProps<TEvent>,
): React.ReactElement;
function ThreadListProvider(props: ThreadListProviderProps): React.ReactElement;
Purpose: provide @anvia/react hook results and app-owned thread-list controllers to UI
primitives without prop drilling.
Shared primitive types
type PrimitiveProps<TElement extends React.ElementType = "div"> =
React.ComponentPropsWithoutRef<TElement> & {
asChild?: boolean;
};
type PrimitiveRef<TElement extends React.ElementType> =
React.ComponentPropsWithRef<TElement>["ref"];
type ThreadContextValue = {
viewportRef: React.MutableRefObject<HTMLElement | null>;
atBottom: boolean;
setAtBottom(atBottom: boolean): void;
scrollToBottom(behavior?: ScrollBehavior): void;
};
type ComposerContextValue = {
input: string;
setInput(input: string): void;
attachments: UIAttachment[];
setAttachments(update: ComposerAttachmentsUpdate): void;
addAttachment(attachment: File | CreateUIAttachment): Promise<void>;
removeAttachment(id: string): void;
clearAttachments(): void;
entities: ComposerEntity[];
setEntities(update: ComposerEntitiesUpdate): void;
quote?: ComposerQuote;
setQuote(quote: ComposerQuote | undefined): void;
clearQuote(): void;
triggers: ComposerTriggerDefinition[];
activeTrigger?: ComposerTriggerState;
setActiveTrigger(update: ComposerTriggerStateUpdate): void;
submit(): Promise<void>;
stop(): void;
status: ChatController["status"];
canSubmit: boolean;
canStop: boolean;
};
type ComposerQuote = {
text: string;
messageId: string;
};
type CompletionInputContextValue = {
input: string;
setInput(input: string): void;
submit(): Promise<void>;
stop(): void;
status: CompletionController["status"];
canSubmit: boolean;
canStop: boolean;
};
type ComposerAttachmentInput = File | CreateUIAttachment;
type ComposerAttachmentsUpdate =
| UIAttachment[]
| ((attachments: UIAttachment[]) => UIAttachment[]);
type ComposerEntitiesUpdate =
| ComposerEntity[]
| ((entities: ComposerEntity[]) => ComposerEntity[]);
type ComposerEntityData =
| string
| number
| boolean
| null
| ComposerEntityData[]
| { [key: string]: ComposerEntityData | undefined };
type ComposerTriggerItem = {
id: string;
label: string;
text?: string;
detail?: React.ReactNode;
data?: ComposerEntityData;
disabled?: boolean;
};
type ComposerTriggerItemsArgs = {
trigger: ComposerTriggerDefinition;
query: string;
input: string;
entities: ComposerEntity[];
signal: AbortSignal;
};
type ComposerTriggerItems =
| ComposerTriggerItem[]
| ((args: ComposerTriggerItemsArgs) => ComposerTriggerItem[] | Promise<ComposerTriggerItem[]>);
type ComposerTriggerDefinition = {
id: string;
char: string;
items: ComposerTriggerItems;
minQueryLength?: number;
allowedPrefixes?: string[] | null;
startOfLine?: boolean;
allowSpaces?: boolean;
};
type ComposerEntity = {
id: string;
triggerId: string;
trigger: string;
label: string;
text: string;
range: {
from: number;
to: number;
};
data?: ComposerEntityData;
};
type ComposerMessageMetadata = {
composer: {
entities: ComposerEntity[];
};
};
type ComposerTriggerState = {
trigger: ComposerTriggerDefinition;
query: string;
items: ComposerTriggerItem[];
loading: boolean;
selectedIndex: number;
rect?: {
top: number;
right: number;
bottom: number;
left: number;
width: number;
height: number;
};
selectItem(item: ComposerTriggerItem): void;
setSelectedIndex(index: number): void;
};
type ComposerTriggerStateUpdate =
| ComposerTriggerState
| undefined
| ((state: ComposerTriggerState | undefined) => ComposerTriggerState | undefined);
type ComposerSubmitMessageArgs<TEvent = unknown> = {
input: string;
attachments: UIAttachment[];
entities: ComposerEntity[];
chat: ChatController<TEvent>;
quote?: ComposerQuote;
clear(): void;
};
type ComposerSubmitMessage<TEvent = unknown> = (
args: ComposerSubmitMessageArgs<TEvent>,
) => Promise<void> | void;
Composer.Root also accepts defaultInput, defaultAttachments, defaultEntities,
defaultQuote, input, onInputChange, attachments, onAttachmentsChange, entities,
onEntitiesChange, quote, onQuoteChange, and triggers for uncontrolled or controlled
composer state. Use submitMessage to replace the default send behavior when an app needs custom
message payloads or metadata. For trigger usage, see
Composer triggers.
Context hooks
function useChatContext<TEvent = unknown>(): ChatController<TEvent>;
function useCompletionContext<TEvent = unknown>(): CompletionController<TEvent>;
function useThread(): ThreadContextValue;
function useComposer(): ComposerContextValue;
function useCompletionInput(): CompletionInputContextValue;
function useAttachment(): AttachmentContextValue;
function useImage(): ImageContextValue;
type MessageContextValue = { message: UIMessage };
type MessagePartContextValue = { part: UIMessagePart };
type MessagePartsFilter = (part: UIMessagePart) => boolean;
type MessageToolPart = Extract<UIMessagePart, { type: "tool" }>;
type MessageAttachmentPart = Extract<UIMessagePart, { type: "attachment" }>;
type MessageToolRenderWhen = "always" | "pending" | "settled";
function useMessage(): MessageContextValue;
function useMessagePart(): MessagePartContextValue;
type ApprovalContextValue = {
approval: ToolApproval;
reason: string;
setReason(reason: string): void;
};
type AttachmentContextValue = {
attachment: UIAttachment;
remove?(): void;
};
type QuestionContextValue = {
question: ToolQuestion;
answers: Record<string, ToolQuestionAnswer>;
setAnswer(prompt: ToolQuestionPrompt, answer: ToolQuestionAnswer): void;
};
type QuestionPromptContextValue = { prompt: ToolQuestionPrompt };
type ImageContextValue = {
attachment: UIAttachment;
src?: string;
name?: string;
mediaType?: string;
isImage: boolean;
zoomOpen: boolean;
setZoomOpen(open: boolean): void;
};
type SelectionToolbarSelection = {
text: string;
messageId: string;
rect: DOMRect;
};
type SelectionToolbarContextValue = {
selection?: SelectionToolbarSelection;
quote(): void;
copy(): Promise<void>;
clear(): void;
};
type ThreadListItemContextValue = {
thread: ThreadListRecord;
active: boolean;
};
function useHumanInput(): UseChatResult["humanInput"];
function useApproval(): ApprovalContextValue;
function useQuestion(): QuestionContextValue;
function useQuestionPrompt(): QuestionPromptContextValue;
function useSelectionToolbar(): SelectionToolbarContextValue;
function useThreadList(): ThreadListController;
function useThreadListItem(): ThreadListItemContextValue;
Purpose: read primitive-local state when composing custom renderers.
Message.Tool accepts either React children or a render function:
<Message.Tool>
{(tool: MessageToolPart) => <ToolCard input={tool.input} output={tool.output} />}
</Message.Tool>
Message.Tool also accepts renderWhen?: MessageToolRenderWhen, and Message.Parts accepts
filter?: MessagePartsFilter.
Composer.Attachments, Thread.Messages, Thread.Suggestions, HumanInput.Approvals, and
HumanInput.Questions accept keepMounted?: boolean for layout control when their collection is
empty.
Image.Root reads the current attachment context or an explicit attachment prop. Use it inside
Message.Attachment or Attachment.Root for image previews, image copy/download actions, and a
portal zoom overlay.
SelectionToolbar.Root renders only when selected text is fully inside one Message.Root. Use
onQuote with controlled Composer.Root quote/onQuoteChange state to bridge selected text into
Composer.Quote.
ThreadListProvider accepts an app-owned ThreadListController. ThreadList.Items renders
non-archived threads by default and accepts archived for archived lists.
Styling
Import @anvia/react-ui/styles.css for functional prototype styling, or target the emitted
data-anvia-* attributes directly. Standalone StreamMarkdown consumers can import only
@anvia/react-ui/stream/styles.css. Application CSS owns layout, spacing, colors, and cards.
