Custom chrome with Companion lifecycle hooks
Goal: Use the Companion widget's lifecycle hooks and imperative handle to build custom UI effects — header highlights, scroll-triggered reveals, open/close buttons — without forking the widget core.
The hook API
The widget exposes four lifecycle callbacks and a programmatic handle:
| Hook / method | When it fires / what it does |
|---|---|
onExpand | User opens the chat panel (mote click) |
onMinimize | User closes the chat panel |
onMessage(msg) | After each message is appended (role: user or assistant) |
onAuthChange(binding) | On login, logout, or org switch |
handle.expand() | Programmatically open the panel |
handle.minimize() | Programmatically close the panel |
handle.setInputAugmenter(fn) | Rewire buildAgentInput without reinit |
handle.destroy() | Unmount the widget |
Header highlight on expand/minimize
The HUMΛN marketing site uses this pattern — the header gets a subtle glow when Companion is open:
import { useState } from 'react';
import { CompanionWidget } from '@human/companion-widget/react';
export function Layout({ children }) {
const [companionOpen, setCompanionOpen] = useState(false);
return (
<>
<header className={companionOpen ? 'header header--companion-active' : 'header'}>
{/* navigation */}
</header>
{children}
<CompanionWidget
humanApiUrl={process.env.NEXT_PUBLIC_API_URL}
agentsCallUrl="/api/companion/ask"
onExpand={() => setCompanionOpen(true)}
onMinimize={() => setCompanionOpen(false)}
ui={{ theme: 'dark', position: 'bottom-right' }}
/>
</>
);
}
Custom open button
Use position: 'inline' and imperative expand() / minimize() to wire your own trigger button:
import { useRef } from 'react';
import { CompanionWidget, CompanionWidgetRef } from '@human/companion-widget/react';
export function SupportPage() {
const widgetRef = useRef<CompanionWidgetRef>(null);
return (
<div>
<button onClick={() => widgetRef.current?.expand()}>
Chat with support
</button>
<CompanionWidget
ref={widgetRef}
humanApiUrl={process.env.NEXT_PUBLIC_API_URL}
agentsCallUrl="/api/companion/ask"
buildAgentInput={() => ({
deployment_id: 'dep_support',
surface_context: { surface: 'support', page: window.location.pathname },
})}
ui={{ theme: 'light', position: 'inline' }}
style={{ width: '100%', height: 500 }}
/>
</div>
);
}
Analytics on message events
<CompanionWidget
...rest
onMessage={(msg) => {
if (msg.role === 'user') {
analytics.track('companion_message_sent', {
surface: 'developer-portal',
page: window.location.pathname,
});
}
}}
/>
Responding to auth changes
Show a personalised greeting when the user logs in:
<CompanionWidget
...rest
onAuthChange={(binding) => {
if (binding.kind !== 'neutral') {
toast.success(`Welcome back, ${binding.displayName ?? 'there'}!`);
}
}}
/>
Vanilla JS (script tag)
const widget = HUMAN.Companion.init({
agentsCallUrl: '/api/companion/ask',
onExpand: () => document.querySelector('.site-header').classList.add('has-companion'),
onMinimize: () => document.querySelector('.site-header').classList.remove('has-companion'),
onMessage: (msg) => console.log('[companion]', msg.role, msg.content.substring(0, 60)),
});
// Open programmatically from a button
document.getElementById('open-companion').addEventListener('click', () => widget.expand());