reachat is a React UI library for building chat/LLM experiences. It provides customizable, composable components for building chat interfaces with support for markdown rendering, rich text input with mentions and slash commands, file uploads, session management, and theming via Tailwind CSS.
- Repository: reaviz/reachat
- License: Apache-2.0
- Package Manager: pnpm (v9.5.0)
- Documentation: https://reachat.dev
- Storybook: https://storybook.reachat.dev
| Technology | Version | Purpose |
|---|---|---|
| React | 18+ | UI framework |
| TypeScript | 4.9.5 | Type safety |
| Tailwind CSS | 4.x | Styling |
| Vite | 5.x | Build tool & dev server |
| Storybook | 8.x | Component development |
| Vitest | 1.x | Testing |
| reablocks | 9.x | Base UI components |
| Tiptap | 3.x | Rich text editor framework |
| Floating UI | 0.27.x | Popup positioning |
| motion | 12.x | Animations |
| Zod | 3.x / 4.x | Runtime prop validation |
reachat/
├── src/ # Source code
│ ├── index.ts # Main entry point - exports all public APIs
│ ├── types.ts # Core TypeScript interfaces
│ ├── theme.ts # Theme system definitions
│ ├── Chat.tsx # Root Chat component
│ ├── ChatContext.ts # React context for chat state
│ ├── AppBar/ # App bar component
│ ├── ChatBubble/ # Chat bubble component
│ ├── ChatInput/ # Input field components
│ ├── ChatSuggestions/ # Suggestion chips component
│ ├── ComponentCatalog/ # Dynamic component rendering system
│ ├── Markdown/ # Markdown rendering (code, tables, etc.)
│ ├── MessageStatus/ # Loading/status indicators
│ ├── SessionMessages/ # Message display components
│ ├── SessionsList/ # Session list/grouping components
│ ├── utils/ # Utility functions
│ └── assets/ # SVG icons
├── stories/ # Storybook stories and examples
├── .storybook/ # Storybook configuration
├── dist/ # Build output (generated)
└── scripts/ # Build scripts
# Install dependencies
npm install
# Start Storybook development server (port 9009)
npm start
# Run tests
npm test
# Run tests with coverage
npm run test:coverage
# Build library for production
npm run build
# Lint code
npm run lint
# Fix lint issues
npm run lint:fix
# Format code with Prettier
npm run prettier
# Build Storybook for deployment
npm run build-storybookThe library uses a composable slot-based architecture. The main Chat component wraps children and provides context:
<Chat sessions={sessions} activeSessionId={activeId}>
<SessionMessagePanel>
<SessionMessages />
<ChatInput />
</SessionMessagePanel>
</Chat>A session is a flat, ordered list of role-tagged messages. This replaces
the old question/response pair model so agentic transcripts (tool calls,
consecutive assistant messages, system notices) can be represented.
// Core data structures in src/types.ts
type MessageRole = 'user' | 'assistant' | 'system' | 'tool' | (string & {});
/** Who sent a message — lets multiple people/agents share one session */
interface MessageAuthor {
id?: string;
name: string;
/** Image URL or custom node */
avatar?: string | ReactNode;
}
interface Message {
id: string;
/** What kind of participant wrote the message */
role: MessageRole;
/** Which participant wrote it — renders an avatar + name header */
author?: MessageAuthor;
/** Markdown content of the message */
content: string;
createdAt?: Date;
updatedAt?: Date;
/** Sources referenced by this message (typically assistant messages) */
sources?: ConversationSource[];
/** Files attached to this message (typically user messages) */
files?: ConversationFile[];
/** Arbitrary structured data, eg. `{ toolCallId, toolCallName, args }` */
metadata?: Record<string, any>;
}
interface Session {
id: string;
title?: string;
createdAt?: Date;
updatedAt?: Date;
messages?: Message[];
/** @deprecated Use `messages` — auto-converted internally */
conversations?: Conversation[];
}MessageRole is open-ended — custom role strings are allowed and fall back to
the assistant presentation in SessionMessage.
role and author are orthogonal: role picks the presentation (user
bubble, assistant, system, tool), author identifies the participant. Multiple
humans share role: 'user' with different authors; multiple agents either
share role: 'assistant' or use custom role strings (eg. 'researcher').
When message.author is set, SessionMessage renders a MessageAuthorBadge
header (avatar + name) above the default body (opt out with
showAuthor={false}). Custom children replace the whole default template;
compose the exported MessageAuthorBadge when a custom template should retain
the author header. See stories/MultiParty.stories.tsx for multi-agent,
group-chat and per-participant-styling demos.
The legacy Conversation shape ({ id, createdAt, question, response?, sources?, files? }) is still exported and still accepted on
Session.conversations, but is @deprecated. Two helpers in
src/utils/messages.ts (both public exports) handle the conversion:
/** Each conversation becomes a `user` message and, when a response exists, an `assistant` message. */
conversationsToMessages(conversations: Conversation[]): Message[];
/** Returns `session.messages`, falling back to converting `session.conversations`. */
getSessionMessages(session?: Session | null): Message[];Generated ids are ${conversation.id}-question and
${conversation.id}-response. When both messages and conversations are
present, messages wins. All internal consumers read messages exclusively
through getSessionMessages() — do the same in new code rather than reading
session.messages directly.
SessionMessages normalizes the active session with getSessionMessages(),
paginates over the resulting Message[], and renders one SessionMessage
card per message. Its render prop receives the message list:
<SessionMessages>
{(messages: Message[]) =>
messages.map((message, i) => (
<SessionMessage
key={message.id}
message={message}
isLast={i === messages.length - 1}
/>
))
}
</SessionMessages>SessionMessage picks its presentation from message.role:
| Role | Presentation |
|---|---|
user |
files + markdown + long-content expand overlay |
assistant |
markdown + MessageSources + MessageActions + loading cursor when isLast && isLoading |
system / tool |
assistant-style content with the role's theme class; no actions or cursor |
| custom (eg. agent roles) | full assistant presentation — theme class falls back to assistant, and actions + loading cursor are included |
Any message with an author additionally gets a MessageAuthorBadge header
above its body.
When isLoading is true and the last message has role: 'user',
SessionMessages renders a pending assistant placeholder with the blinking
cursor after it.
MessageActions takes message={message} (copy copies message.content) and
is rendered on assistant messages by default. MessageQuestion and
MessageResponse remain as @deprecated thin wrappers over MessageContent
so existing custom renderers keep compiling.
Three view modes are supported:
console- Full screen with sessions sidebarcompanion- Compact/mobile viewchat- Chat only, no sessions list
The theme is defined in src/theme.ts using a typed object with Tailwind classes:
const chatTheme: ChatTheme = {
base: 'dark:text-white text-gray-500',
console: 'flex w-full gap-4 h-full',
// ... nested theme objects for each component
};Components use the theme via useComponentTheme from reablocks:
const theme = useComponentTheme<ChatTheme>('chat', customTheme);Message styling is keyed by role under messages.message:
messages: {
message: {
base: string;
user: string; // was: question
assistant: string; // was: response
system: string; // new — muted, centered informational style
tool: string; // new — compact style for tool activity
author: { // new — MessageAuthorBadge (avatar + name header)
base: string;
avatar: string;
name: string;
};
cursor: string;
overlay: string;
expand: string;
// ...files, sources, markdown, footer, scrollToBottom
}
}The library includes an advanced rich text input system built on Tiptap v3 with support for mentions and slash commands.
Located in src/ChatInput/RichTextInput.tsx, this component provides:
- Auto-expanding textarea with configurable min/max heights
- Mentions support - Trigger with
@to mention users, files, or custom entities - Slash commands - Trigger with
/for quick actions - Custom keyboard handling - Shift+Enter for multi-line, Enter to submit
- Floating suggestions - Smart popup positioning using Floating UI
- Keyboard navigation - Arrow keys, Enter/Tab to select, Escape to close
Exposed Methods via Ref:
interface RichTextInputRef {
focus: () => void;
getValue: () => string;
setValue: (value: string) => void;
insertText: (text: string) => void;
}Usage Example:
<ChatInput
mentions={{
trigger: '@',
items: [
{ id: '1', label: 'John Doe', description: 'Product Manager' },
{ id: '2', label: 'Jane Smith', description: 'Engineer' }
]
}}
commands={{
trigger: '/',
items: [
{ id: 'help', label: 'Help', description: 'Get help', type: 'action' },
{ id: 'search', label: 'Search', description: 'Search docs', type: 'insert' }
]
}}
/>Located in src/ChatInput/MentionList.tsx, this floating popup component:
- Displays suggestion items with keyboard navigation
- Auto-scrolls to keep selected item visible
- Supports custom rendering via
renderItemandrenderEmptycallbacks - Full ARIA accessibility attributes
- Smart positioning to stay within viewport bounds
Core types defined in src/ChatInput/types.ts:
// Base suggestion item
interface SuggestionItem {
id: string;
label: string;
description?: string;
icon?: ReactNode;
metadata?: Record<string, any>;
}
// For @mentions
interface MentionItem extends SuggestionItem {
value?: string; // Override display value
}
// For /commands
interface SlashCommandItem extends SuggestionItem {
shortcut?: string; // Keyboard shortcut hint
type?: 'insert' | 'action';
}
// Configuration for suggestions
interface SuggestionConfig<T = SuggestionItem> {
trigger: string;
items?: T[];
onSearch?: (query: string) => Promise<T[]>;
onSelect?: (item: T) => void;
maxResults?: number;
renderItem?: (item: T) => ReactNode;
renderEmpty?: () => ReactNode;
}New theme sections in src/theme.ts:
input: {
popup: {
base: string; // Popup container styles
content: string; // List content wrapper
item: string; // Individual item
itemHighlighted: string; // Active/selected item
itemIcon: string; // Icon wrapper
itemContent: string; // Text content wrapper
itemLabel: string; // Primary label
itemDescription: string; // Secondary description
itemShortcut: string; // Shortcut hint
empty: string; // Empty state
loading: string; // Loading state
},
tag: {
base: string; // Tag styles in editor
mention: string; // Mention-specific styles
command: string; // Command-specific styles
},
editor: {
base: string; // Editor wrapper
container: string; // Content container
placeholder: string; // Placeholder text
}
}The library includes a ComponentCatalog system that allows LLMs to render custom React components via JSON specifications inside fenced code blocks. Located in src/ComponentCatalog/.
See src/ComponentCatalog/README.md for full usage documentation with examples.
- Developer defines components with Zod schemas via
componentCatalog() - The catalog is passed to
<Chat components={catalog}>which wires in a remark plugin and<pre>override - When the LLM emits a
```componentcode block containing JSON, the system validates the spec against the Zod schema and renders the matching React component catalog.systemPrompt()generates LLM instructions describing available components
| File | Purpose |
|---|---|
componentCatalog.ts |
Main factory function — creates the catalog object |
types.ts |
TypeScript interfaces (ComponentDefinition, ComponentSpec, etc.) |
ComponentPre.tsx |
<pre> override that intercepts code blocks by language tag |
ComponentRenderer.tsx |
Validates JSON and renders components with error boundary |
validateSpec.ts |
Four-step validation pipeline (JSON parse, lookup, Zod, children) |
generatePrompt.ts |
Generates LLM system prompt from definitions |
ComponentError.tsx |
Default error display component |
chartComponentDef.tsx |
Pre-built chart component definition using reaviz |
import { componentCatalog } from 'reachat';
import { z } from 'zod';
const catalog = componentCatalog({
WeatherCard: {
description: 'Displays weather for a city',
props: z.object({
city: z.string(),
temperature: z.number()
}),
component: ({ city, temperature }) => (
<div>{city}: {temperature}°F</div>
)
}
});
<Chat sessions={sessions} components={catalog}>
<SessionMessages />
<ChatInput />
</Chat>The LLM emits:
\`\`\`component
{ "type": "WeatherCard", "props": { "city": "SF", "temperature": 68 } }
\`\`\`
- Single:
{ "type": "Name", "props": { ... } } - Multiple:
[{ "type": "A", "props": {} }, { "type": "B", "props": {} }] - Nested:
{ "type": "Parent", "props": {}, "children": [{ "type": "Child", "props": {} }] }
Four error types: invalid_json, unknown_component, invalid_props, render_error. Each component is wrapped in a React error boundary. Custom error UI via onError callback in options.
- zod — regular dependency, used for runtime prop validation and system prompt generation
- reaviz (optional peer dep) — required only when using
createChartComponentDef()
Tests are co-located in the ComponentCatalog/ directory:
componentCatalog.spec.ts— factory function testsvalidateSpec.spec.ts— 18 validation pipeline test casesgeneratePrompt.spec.ts— system prompt generation testschartComponentDef.spec.ts— chart definition validation tests
Use @/ for absolute imports from src/:
// Good
import { ChatContext } from '@/ChatContext';
// Avoid relative paths across directories
import { ChatContext } from '../../../ChatContext'; // BadThe ESLint rule no-relative-import-paths enforces this (same folder imports are allowed).
- Functional Components with TypeScript:
interface ComponentProps {
/** JSDoc comment for prop */
propName: string;
}
export const Component: FC<ComponentProps> = ({ propName }) => {
// ...
};- Forward Refs when exposing methods:
export interface ComponentRef {
focus: () => void;
}
export const Component = forwardRef<ComponentRef, ComponentProps>((props, ref) => {
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus()
}));
});- Context consumption:
const { theme, isLoading, sendMessage } = useContext(ChatContext);Each component module follows this structure:
ComponentName/
├── index.ts # Re-exports public APIs
├── ComponentName.tsx # Main component
└── SubComponent.tsx # Related sub-components
- Use Tailwind CSS classes via the theme system
- Use
cn()from reablocks for conditional class merging:
<div className={cn(theme.base, { [theme.active]: isActive })} />- Dark mode uses
dark:prefix in Tailwind classes - Theme tokens defined in
src/index.cssusing@theme inline
- Semicolons: Required
- Quotes: Single quotes
- Trailing commas: None
- Indentation: 2 spaces
- Line width: 80 characters
Tests use Vitest with jsdom environment. Test files are co-located with source:
utils/
├── grouping.ts
└── grouping.spec.ts
Run tests:
npm test # Watch mode
npm run test:coverage # With coverage reportStories are located in /stories/ directory and follow the pattern:
import { Meta } from '@storybook/react';
export default {
title: 'Demos/ComponentName',
component: ComponentName
} as Meta;
export const Default = () => <ComponentName />;
export const WithProps = () => <ComponentName prop="value" />;The build creates three outputs (ESM-only — no UMD/CJS):
- ESM (
dist/index.js) - Modern ES modules - CSS (
dist/index.css) - Tailwind-compiled styles - Types (
dist/index.d.ts) - TypeScript declarations
- reablocks: Base component library (Button, Textarea, etc.)
- reakeys: Keyboard shortcuts
- react-markdown: Markdown rendering
- react-syntax-highlighter: Code highlighting
- date-fns: Date utilities
- lodash: Utility functions
- @tiptap/react: Rich text editor framework (v3.x) with extensions for:
- Document/paragraph/text structure
- Hard breaks and placeholders
- Mention support for @mentions
- @floating-ui/react: Smart popup positioning for suggestion dropdowns
- zod: Runtime prop validation for ComponentCatalog
- Create directory:
src/NewComponent/ - Create main component file:
NewComponent.tsx - Create index.ts with exports
- Add theme properties to
ChatThemeinsrc/theme.ts - Export from
src/index.ts - Create story in
stories/NewComponent.stories.tsx
- Update interface in
src/theme.ts(ChatTheme) - Add default values in
chatThemeobject - Use via
theme.newPropertyin components
- Check existing plugins in
src/Markdown/plugins/ - Add new remark/rehype plugins to the
remarkPluginsprop - Custom renderers go in the Markdown component
The ChatInput component accepts mentions and commands props for rich text functionality:
Static Items:
<ChatInput
mentions={{
trigger: '@',
items: [
{ id: '1', label: 'User Name', description: 'Role', icon: <Icon /> }
]
}}
/>Dynamic Search:
<ChatInput
commands={{
trigger: '/',
onSearch: async (query) => {
const results = await searchAPI(query);
return results.map(r => ({ id: r.id, label: r.name }));
},
maxResults: 10
}}
/>Custom Selection Handler:
<ChatInput
mentions={{
trigger: '@',
items: mentionItems,
onSelect: (item) => {
console.log('Selected:', item);
// Handle custom logic
}
}}
/>Custom Rendering:
<ChatInput
commands={{
trigger: '/',
items: commandItems,
renderItem: (item) => (
<div>
<strong>{item.label}</strong>
<span>{item.shortcut}</span>
</div>
),
renderEmpty: () => <div>No commands found</div>
}}
/>- The library is designed for React 18+
- All components support dark/light themes
- CSS is injected via JS for library builds (vite-plugin-css-injected-by-js)
- SVGs are imported as React components using vite-plugin-svgr
- The package uses ES modules (
"type": "module") - Rich text input uses Tiptap v3 with a document/paragraph/text node structure
- Suggestion popups use Floating UI with flip/shift middleware for smart positioning
- Accessibility: All interactive components include proper ARIA attributes
- Pre-commit hooks run via Husky
- Prettier formats staged files automatically
- Follow conventional commit messages