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.
Quick Example
import * as Primitive from "@cossistant/react/primitives";
<Primitive.PageRegistryProvider>
<Primitive.Page name="HOME" component={HomePage} />
<Primitive.Page name="SETTINGS" component={SettingsPage} />
<Primitive.Window>
{({ isOpen, close }) => (
<div className="fixed inset-0 bg-black/50">
<Primitive.Router page={currentPage} params={params} />
</div>
)}
</Primitive.Window>
<Primitive.Bubble>
{({ toggle }) => (
<button onClick={toggle}>Open Chat</button>
)}
</Primitive.Bubble>
</Primitive.PageRegistryProvider>Primitives Reference
Layout & Structure
<PageRegistryProvider>
Context provider for declarative page registration. Wrap your app to enable <Page> and <Router>.
<PageRegistryProvider>
<Page name="HOME" component={HomePage} />
<Router page={currentPage} />
</PageRegistryProvider><Router>
Generic router that renders registered pages.
<Router
page={currentPage} // Current page name
params={params} // Params to pass
fallback={NotFoundPage} // Optional fallback
>
<Page name="HOME" component={HomePage} />
</Router><Page>
Declaratively register a page component.
<Page name="SETTINGS" component={SettingsPage} /><Window>
Dialog container with open/close state and escape key handling.
<Window isOpen={isOpen} onOpenChange={setOpen}>
{({ isOpen, close }) => (
<div>
{isOpen && <p>Dialog content</p>}
<button onClick={close}>Close</button>
</div>
)}
</Window><Bubble>
Floating action button with widget state.
<Bubble>
{({ isOpen, unreadCount, isTyping, toggle }) => (
<button onClick={toggle}>
{isOpen ? "×" : "💬"}
{unreadCount > 0 && <span>{unreadCount}</span>}
</button>
)}
</Bubble>Timeline & Messages
<ConversationTimeline>
Message timeline with automatic scrolling and loading states.
<ConversationTimeline items={messages} isLoading={loading}>
{(item) => <TimelineItem item={item} />}
</ConversationTimeline><TimelineItem>
Individual message or event in the timeline.
<TimelineItem>
<TimelineItemContent>{message.text}</TimelineItemContent>
<TimelineItemTimestamp>{message.createdAt}</TimelineItemTimestamp>
</TimelineItem><TimelineItemGroup>
Group consecutive messages by the same sender.
<TimelineItemGroup>
<TimelineItemGroupHeader>
<TimelineItemGroupAvatar src={user.image} />
<span>{user.name}</span>
</TimelineItemGroupHeader>
<TimelineItemGroupContent>
{messages.map(msg => <TimelineItem key={msg.id} {...msg} />)}
</TimelineItemGroupContent>
</TimelineItemGroup>Input & Interaction
<MultimodalInput>
Rich text input with file upload support.
<MultimodalInput
value={text}
onChange={setText}
onSubmit={handleSend}
placeholder="Type a message..."
/><FileInput>
File upload component with drag-and-drop.
<FileInput
onFilesSelected={handleFiles}
accept="image/*,application/pdf"
maxFiles={5}
/><Button>
Accessible button with variants.
<Button variant="primary" size="large" onClick={handleClick}>
Click me
</Button>Visual
<Avatar>, <AvatarImage>, <AvatarFallback>
User avatar with image and fallback.
<Avatar>
<AvatarImage src={user.image} alt={user.name} />
<AvatarFallback>{user.initials}</AvatarFallback>
</Avatar><TypingIndicator>
Animated typing indicator.
<TypingIndicator
participants={[
{ id: "1", name: "Alice", type: "user" }
]}
/>Configuration
<Config>
Configure widget behavior per route or page.
<Config
quickOptions={["Pricing", "Features", "Support"]}
defaultMessages={[{ text: "Hi! How can we help?" }]}
/>Building from Scratch
Create a completely custom support experience:
import * as Primitive from "@cossistant/react/primitives";
import { useSupportStore } from "@cossistant/react";
export function CustomSupport() {
const { isOpen, toggle } = useSupportStore();
const [page, setPage] = useState("HOME");
return (
<Primitive.PageRegistryProvider>
<Primitive.Page name="HOME" component={CustomHomePage} />
<Primitive.Page name="CHAT" component={CustomChatPage} />
{/* Custom bubble */}
<Primitive.Bubble>
{({ toggle, unreadCount }) => (
<button
onClick={toggle}
className="fixed bottom-4 right-4 rounded-full bg-blue-600 p-4"
>
💬 {unreadCount > 0 && `(${unreadCount})`}
</button>
)}
</Primitive.Bubble>
{/* Custom window */}
<Primitive.Window isOpen={isOpen}>
{({ close }) => (
<div className="fixed inset-0 flex items-end justify-end p-4">
<div className="w-96 h-[600px] bg-white rounded-lg shadow-xl">
<button onClick={close}>×</button>
<Primitive.Router page={page} fallback={CustomHomePage} />
</div>
</div>
)}
</Primitive.Window>
</Primitive.PageRegistryProvider>
);
}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. Principles work in any React environment.
Use the <Support /> component for quick setup. Use primitives when you need complete freedom.