Headless, composable building blocks for support experiences.

Philosophy

Primitives are headless UI components that give you complete control over styling and behavior. Inspired by shadcn/ui, they follow these principles:

  • Unstyled by default - Bring your own Tailwind classes
  • Fully composable - Build complex UIs from simple pieces
  • Developer-owned - Copy, modify, and extend as needed
  • Accessible - Built-in ARIA patterns and keyboard navigation

Use primitives when you need complete control over the support experience.

Import

Access primitives via the Primitives namespace:

import { Primitives } from "@cossistant/react";
 
// Use individual primitives
<Primitives.Trigger>...</Primitives.Trigger>
<Primitives.Window>...</Primitives.Window>
<Primitives.Avatar>...</Primitives.Avatar>

Or import the namespace with an alias:

import { Primitives as P } from "@cossistant/react";
 
<P.Trigger>...</P.Trigger>

Quick Example

import { Primitives, useSupportConfig } from "@cossistant/react";
 
function CustomWidget() {
  const { isOpen, toggle } = useSupportConfig();
 
  return (
    <>
      <Primitives.Trigger>
        {({ toggle, unreadCount }) => (
          <button onClick={toggle} type="button">
            {unreadCount > 0 ? `Help (${unreadCount})` : "Help"}
          </button>
        )}
      </Primitives.Trigger>
 
      <Primitives.Window>
        {({ isOpen, close }) => (
          isOpen && (
            <div className="fixed bottom-20 right-4 w-96 bg-white rounded-lg shadow-xl">
              <button onClick={close} type="button">×</button>
              <p>Custom support content</p>
            </div>
          )
        )}
      </Primitives.Window>
    </>
  );
}

Primitives Reference

Core Components

<Trigger>

Trigger button with widget state. Can be placed anywhere in the DOM.

<Primitives.Trigger>
  {({ isOpen, unreadCount, isTyping, toggle }) => (
    <button onClick={toggle} type="button">
      {isOpen ? "×" : "💬"}
      {unreadCount > 0 && <span>{unreadCount}</span>}
    </button>
  )}
</Primitives.Trigger>

<Window>

Dialog container with open/close state and escape key handling.

<Primitives.Window>
  {({ isOpen, close }) => (
    isOpen && (
      <div className="fixed inset-0 bg-black/50">
        <div className="bg-white p-4 rounded">
          <button onClick={close} type="button">Close</button>
          <p>Window content</p>
        </div>
      </div>
    )
  )}
</Primitives.Window>

Timeline & Messages

<ConversationTimeline>

Message timeline container with automatic scrolling and loading states.

<Primitives.ConversationTimeline>
  <Primitives.ConversationTimelineLoading />
  <Primitives.ConversationTimelineEmpty />
  <Primitives.ConversationTimelineContainer>
    {messages.map(msg => (
      <Primitives.TimelineItem key={msg.id} />
    ))}
  </Primitives.ConversationTimelineContainer>
</Primitives.ConversationTimeline>

<TimelineItem>

Individual message or event in the timeline.

<Primitives.TimelineItem>
  <Primitives.TimelineItemContent>
    {message.text}
  </Primitives.TimelineItemContent>
  <Primitives.TimelineItemTimestamp>
    {message.createdAt}
  </Primitives.TimelineItemTimestamp>
</Primitives.TimelineItem>

<TimelineItemGroup>

Group consecutive messages by the same sender.

<Primitives.TimelineItemGroup>
  <Primitives.TimelineItemGroupHeader>
    <Primitives.TimelineItemGroupAvatar src={user.image} />
    <span>{user.name}</span>
  </Primitives.TimelineItemGroupHeader>
  <Primitives.TimelineItemGroupContent>
    {messages.map(msg => (
      <Primitives.TimelineItem key={msg.id} />
    ))}
  </Primitives.TimelineItemGroupContent>
</Primitives.TimelineItemGroup>

Input & Interaction

<MultimodalInput>

Rich text input with file upload support.

<Primitives.MultimodalInput
  value={text}
  onChange={setText}
  onSubmit={handleSend}
  placeholder="Type a message..."
/>

<FileInput>

File upload component with drag-and-drop.

<Primitives.FileInput
  onFilesSelected={handleFiles}
  accept="image/*,application/pdf"
  maxFiles={5}
/>

<Button>

Accessible button primitive.

<Primitives.Button onClick={handleClick}>
  Send Message
</Primitives.Button>

Visual

<Avatar>, <AvatarImage>, <AvatarFallback>

User avatar with image and fallback.

<Primitives.Avatar>
  <Primitives.AvatarImage src={user.image} alt={user.name} />
  <Primitives.AvatarFallback>{user.initials}</Primitives.AvatarFallback>
</Primitives.Avatar>

<TypingIndicator>

Animated typing indicator showing who's typing.

<Primitives.TypingIndicator
  participants={[
    { id: "1", name: "Alice", type: "user" }
  ]}
/>

Configuration

<Config>

Configure widget behavior per route or page.

<Primitives.Config
  quickOptions={["Pricing", "Features", "Support"]}
  defaultMessages={[
    { content: "Hi! How can we help?", senderType: "ai" }
  ]}
/>

Available Hooks

Use these hooks alongside primitives for complete control:

import { useSupportConfig, useSupportNavigation, useSupport } from "@cossistant/react";

useSupportConfig() - Widget open/close state

const { isOpen, toggle, open, close } = useSupportConfig();

useSupportNavigation() - Page navigation

const { navigate, goBack, canGoBack, page, params } = useSupportNavigation();

useSupport() - Full support context

const { visitor, website, client, unreadCount } = useSupport();

TriggerRenderProps

The <Trigger> component provides these render props:

type TriggerRenderProps = {
  isOpen: boolean;      // Whether the widget is open
  isTyping: boolean;    // Whether someone is typing
  unreadCount: number;  // Number of unread messages
  toggle: () => void;   // Toggle widget open/closed
};

Building from Scratch

Create a completely custom support experience:

import { Primitives, SupportProvider, useSupportConfig } from "@cossistant/react";
import { useState } from "react";
 
function CustomSupport() {
  const { isOpen } = useSupportConfig();
 
  return (
    <>
      {/* Custom trigger */}
      <Primitives.Trigger>
        {({ toggle, unreadCount }) => (
          <button
            onClick={toggle}
            className="fixed bottom-4 right-4 rounded-full bg-blue-600 p-4 text-white"
            type="button"
          >
            💬 {unreadCount > 0 && `(${unreadCount})`}
          </button>
        )}
      </Primitives.Trigger>
 
      {/* Custom window */}
      <Primitives.Window>
        {({ close }) => (
          isOpen && (
            <div className="fixed bottom-20 right-4 w-96 h-[500px] bg-white rounded-lg shadow-xl border">
              <header className="flex items-center justify-between p-4 border-b">
                <h2>Support</h2>
                <button onClick={close} type="button">×</button>
              </header>
              <div className="p-4">
                <p>Your custom support content here</p>
              </div>
            </div>
          )
        )}
      </Primitives.Window>
    </>
  );
}
 
// Wrap with provider
export function App() {
  return (
    <SupportProvider apiKey="pk_xxx">
      <CustomSupport />
    </SupportProvider>
  );
}

Why Primitives?

Full control. No opinionated styles or structure—build exactly what you need.

Type-safe. All primitives are fully typed with TypeScript.

Framework-agnostic patterns. Works in any React environment.

Use the <Support /> component for quick setup. Use primitives when you need complete freedom.