Skip to main content

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 / methodWhen it fires / what it does
onExpandUser opens the chat panel (mote click)
onMinimizeUser 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());

Next steps

← All guides