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.