Style and customize the Support widget with Tailwind classes and compound components.

Overview

The Support widget follows progressive disclosure. Start with zero config, add styling, then compose your own components when needed.

Level 1: Style Overrides

Override styles with classNames:

<Support
  classNames={{
    trigger: "bg-purple-600 hover:bg-purple-700",
    content: "border-purple-200 shadow-2xl",
  }}
/>

Available classNames

  • trigger - The floating chat button
  • content - The main widget dialog

Level 2: Positioning

Control where the content appears relative to the trigger:

<Support
  side="bottom"    // "top" | "bottom" | "left" | "right"
  align="end"      // "start" | "center" | "end"
  sideOffset={16}  // Distance in pixels
/>

Auto Collision Avoidance

The widget automatically repositions itself to stay within the viewport. When there isn't enough space on the preferred side, it flips to the opposite side. It also shifts along the axis to prevent clipping at screen edges.

// Collision avoidance is enabled by default
<Support side="top" align="end" />
 
// Disable collision avoidance for static positioning
<Support avoidCollisions={false} />
 
// Custom padding from viewport edges (default: 8px)
<Support collisionPadding={16} />
 
// Per-side collision padding
<Support collisionPadding={{ top: 8, bottom: 32, left: 8, right: 8 }} />

Level 3: Custom Trigger

Replace the default trigger with <Support.Trigger>:

import { Support, type TriggerRenderProps } from "@cossistant/react";
 
<Support side="bottom" align="end">
  <Support.Trigger className="flex items-center gap-2 px-4 py-2 bg-blue-600 rounded-lg">
    {({ isOpen, unreadCount }: TriggerRenderProps) => (
      <>
        <span>{isOpen ? "Close" : "Help"}</span>
        {unreadCount > 0 && (
          <span className="bg-red-500 text-white text-xs px-1.5 rounded-full">
            {unreadCount}
          </span>
        )}
      </>
    )}
  </Support.Trigger>
</Support>

Trigger Render Props

PropTypeDescription
isOpenbooleanWhether the widget is currently open
unreadCountnumberNumber of unread messages
isTypingbooleanWhether an agent is typing
toggle() => voidFunction to toggle widget open/closed

Level 4: Custom Content

Control the content positioning and styling with <Support.Content>:

<Support>
  <Support.Trigger className="...">
    {(props) => <MyTriggerContent {...props} />}
  </Support.Trigger>
  <Support.Content
    side="bottom"
    align="end"
    sideOffset={8}
    className="border-2 border-purple-500"
  >
    <Support.Router />
  </Support.Content>
</Support>

Level 5: Full Composition

For complete control, use <Support.Root>:

import { Support } from "@cossistant/react";
 
export default function App() {
  return (
    <Support.Root defaultOpen={false} theme="dark">
      <Support.Trigger asChild>
        <button className="px-4 py-2 bg-indigo-600 rounded" type="button">
          Help
        </button>
      </Support.Trigger>
      <Support.Content side="bottom" align="end" className="custom-content">
        <Support.Router />
      </Support.Content>
    </Support.Root>
  );
}

Custom Trigger in a Topbar

A common use case is placing the trigger in your navigation bar:

import { Support, type TriggerRenderProps } from "@cossistant/react";
 
function Topbar() {
  return (
    <header className="flex items-center justify-between px-4 h-16">
      <Logo />
      <nav className="flex items-center gap-4">
        <Link href="/settings">Settings</Link>
        <Support side="bottom" align="end" sideOffset={8}>
          <Support.Trigger className="flex items-center gap-2 px-3 py-2 rounded-md hover:bg-gray-100">
            {({ isOpen, unreadCount }: TriggerRenderProps) => (
              <>
                <span>{isOpen ? "Close" : "Help"}</span>
                {unreadCount > 0 && (
                  <span className="bg-red-500 text-white text-xs px-1.5 rounded-full">
                    {unreadCount}
                  </span>
                )}
              </>
            )}
          </Support.Trigger>
        </Support>
      </nav>
    </header>
  );
}

Custom Pages

Add custom pages to the router:

import { Support, useSupportNavigation } from "@cossistant/react";
 
const FAQPage = () => {
  const { goBack } = useSupportNavigation();
  return (
    <div className="p-4">
      <button onClick={goBack} type="button">← Back</button>
      <h1>Frequently Asked Questions</h1>
    </div>
  );
};
 
<Support>
  <Support.Page name="FAQ" component={FAQPage} />
</Support>

Or with the router directly:

<Support>
  <Support.Content>
    <Support.Router>
      <Support.Page name="FAQ" component={FAQPage} />
      <Support.Page name="SETTINGS" component={SettingsPage} />
    </Support.Router>
  </Support.Content>
</Support>

Type Reference

TriggerRenderProps

type TriggerRenderProps = {
  isOpen: boolean;
  isTyping: boolean;
  unreadCount: number;
  toggle: () => void;
};

ContentProps

type CollisionPadding =
  | number
  | { top?: number; right?: number; bottom?: number; left?: number };
 
type ContentProps = {
  className?: string;
  side?: "top" | "bottom" | "left" | "right";
  align?: "start" | "center" | "end";
  sideOffset?: number;
  avoidCollisions?: boolean;       // default: true
  collisionPadding?: CollisionPadding; // default: 8
  children?: React.ReactNode;
};

SupportProps

type SupportProps = {
  className?: string;
  side?: "top" | "bottom" | "left" | "right";
  align?: "start" | "center" | "end";
  sideOffset?: number;
  avoidCollisions?: boolean;       // default: true
  collisionPadding?: CollisionPadding; // default: 8
  classNames?: {
    trigger?: string;
    content?: string;
  };
  theme?: "light" | "dark";
  defaultOpen?: boolean;
  locale?: string;
  content?: SupportTextContentOverrides;
  quickOptions?: string[];
  defaultMessages?: DefaultMessage[];
  customPages?: CustomPage[];
  children?: React.ReactNode;
};

Full Example

Combining multiple customizations:

import { Support, type TriggerRenderProps } from "@cossistant/react";
import { AnimatePresence, motion } from "motion/react";
import { MessageCircle, X } from "lucide-react";
 
const AnimatedTriggerContent = ({ isOpen, unreadCount }: TriggerRenderProps) => (
  <>
    <AnimatePresence mode="wait">
      {isOpen ? (
        <motion.div
          key="close"
          initial={{ scale: 0.8, opacity: 0 }}
          animate={{ scale: 1, opacity: 1 }}
          exit={{ scale: 0.8, opacity: 0 }}
        >
          <X className="size-5" />
        </motion.div>
      ) : (
        <motion.div
          key="open"
          initial={{ scale: 0.8, opacity: 0 }}
          animate={{ scale: 1, opacity: 1 }}
          exit={{ scale: 0.8, opacity: 0 }}
        >
          <MessageCircle className="size-6" />
        </motion.div>
      )}
    </AnimatePresence>
    {unreadCount > 0 && (
      <span className="absolute -top-1 -right-1 flex size-5 items-center justify-center rounded-full bg-red-500 text-white text-xs">
        {unreadCount}
      </span>
    )}
  </>
);
 
export default function App() {
  return (
    <Support theme="dark" side="top" align="end">
      <Support.Trigger className="relative flex size-14 items-center justify-center rounded-full bg-indigo-600 text-white hover:bg-indigo-700">
        {(props) => <AnimatedTriggerContent {...props} />}
      </Support.Trigger>
      <Support.Content className="border-indigo-200" />
    </Support>
  );
}