Skip to main content

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
Loading chatbot...

What Gets Replaced

renderFooter is a full replacement, not an additive slot:

  • Built-in textarea with 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 footerEndActions slot

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

FieldTypeDescription
sendMessage(params) => voidSubmit a message; undefined in preview / disconnected state
isConnectingbooleanChannel is busy — use this to disable the send button
pendingInputValuestring | nullText pushed in via ChatbotRef.setInputValue
setPendingInputValue(v: string | null) => voidClear the pending value back to null once consumed
inputPlaceholderstringPlaceholder passed through Chatbot props
messagesMap<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.