Custom Footer
Use renderFooter to fully replace the default footer. Reach for it when
you need to redesign the entire input area (single-line input, custom voice
button, completely different layout). If you only want to add a button
next to send, use footerEndActions instead.
Fully Replace the Footer
renderFooter takes over the footer area entirely: the built-in textarea, send button, image / document upload, export, mic, drag-drop, IME guard, and footerEndActions are all removed — the renderer fully owns the footer.
- Read sendMessage / isConnecting / inputPlaceholder from useAsgardContext()
- Pipe external text into the textarea via pendingInputValue + setPendingInputValue
- Disable send while isConnecting to avoid double-sends
- IME composition guard: Enter does not submit while isComposing
What Gets Replaced
renderFooter is a full replacement, not an additive slot:
- Built-in
textareawith auto-resize - Send button and microphone button
- Image upload, document upload, export conversation
- Drag-and-drop upload integration
- IME composition guard (prevents Enter from submitting mid-composition)
- The
footerEndActionsslot
All of the above stop rendering — your renderer owns the entire footer area.
Code Example
import {
Chatbot,
useAsgardContext,
type ChatbotRef,
} from "@asgard-js/react";
import { useEffect, useRef, useState } from "react";
function MyFooter() {
const {
sendMessage,
isConnecting,
pendingInputValue,
setPendingInputValue,
inputPlaceholder,
} = useAsgardContext();
const [value, setValue] = useState("");
const textareaRef = useRef<HTMLTextAreaElement>(null);
// Receive text pushed in via ChatbotRef.setInputValue
useEffect(() => {
if (pendingInputValue == null) return;
setValue(pendingInputValue);
setPendingInputValue(null);
textareaRef.current?.focus();
}, [pendingInputValue, setPendingInputValue]);
const submit = () => {
const text = value.trim();
if (!text || isConnecting) return;
sendMessage?.({ text });
setValue("");
};
return (
<div className="footer">
<textarea
ref={textareaRef}
placeholder={inputPlaceholder}
value={value}
onChange={(e) => setValue(e.target.value)}
onKeyDown={(e) => {
// Skip submit while IME is composing
if (
e.key === "Enter" &&
!e.shiftKey &&
!e.nativeEvent.isComposing
) {
e.preventDefault();
submit();
}
}}
rows={1}
/>
<button
onClick={submit}
disabled={!value.trim() || isConnecting}
>
Send
</button>
</div>
);
}
function Demo() {
const chatbotRef = useRef<ChatbotRef>(null);
return (
<Chatbot
ref={chatbotRef}
renderFooter={() => <MyFooter />}
{...rest}
/>
);
}
useAsgardContext Key Fields
| Field | Type | Description |
|---|---|---|
sendMessage | (params) => void | Submit a message; undefined in preview / disconnected state |
isConnecting | boolean | Channel is busy — use this to disable the send button |
pendingInputValue | string | null | Text pushed in via ChatbotRef.setInputValue |
setPendingInputValue | (v: string | null) => void | Clear the pending value back to null once consumed |
inputPlaceholder | string | Placeholder passed through Chatbot props |
messages | Map<string, ConversationMessage> | All messages (if you need token counts, etc.) |
Wiring renderMenu + ChatbotRef.setInputValue
The demo above also uses renderMenu to list quick prompts. Clicking a
prompt calls chatbotRef.current?.setInputValue(prompt) to push the
text into the SDK's internal pendingInputValue. Your custom footer
consumes it, producing a "click → text appears in textarea → user
decides whether to send" flow.
When Not to Use renderFooter
- Just want a button next to send: use
footerEndActions— built-in logic stays intact. - Skip the Chatbot UI entirely: use Headless mode and assemble the whole interface yourself.