Components

Chat

A composable chat interface with aligned messages, message content, and an accessible composer.

Preview

Hey! How are you?
Doing well — thanks!

More examples

Avatars and message metadata

Combine avatars, presence, timestamps, delivery states, and an interactive composer.

Maya's avatar

Maya from Support

Online

Hi! I found the issue with your preview deployment.

10:24 AM

Great — was it the forwarded host configuration?

10:25 AM · Read

Exactly. I added the two proxy headers and the generated registry URL is correct now.

10:26 AM

Custom message content

Use custom message content for sender names, attachments, dividers, and delivery details.

Design handoff

3 people · Project chat

Alex Kim

The updated conversation flow is ready for review.

FIG

chat-flow.fig

4.2 MB · Figma file

Today
Looks great. I especially like that message content can contain any markup.
Delivered ✓✓

Jordan Smith

Ship it 🚀

Installation

Install directly from the hosted registry with the shadcn-svelte CLI.

Terminal
npx shadcn-svelte@latest add https://ui.akcodeworks.com/r/chat.json

Usage

Compose Root, Messages, Message, Content, and Composer. Message defaults to the right side; pass side="left" for incoming messages. Bind Composer's value and handle messages with onSend.

API reference

PropTypeDefaultDescription
Message.sideleft | rightrightAligns the message with the sender or recipient side of the conversation.
Composer.valuestring''Current text in the message composer.
Composer.onSend(message: string) => void | Promise<void>—Called with the composed message when it is submitted.
Composer.clearOnSendbooleantrueClears the composer after a successful submission.
Composer.submitOnEnterbooleantrueSubmits on Enter while preserving Shift+Enter for a new line.
Composer.disabledbooleanfalsePrevents typing and submission when enabled.

Source

These files are included in the registry item and copied into the consuming project.

chat.svelte
<script lang="ts">	import type { Snippet } from 'svelte';	import type { HTMLAttributes } from 'svelte/elements';	import { cn } from '$lib/utils.js';	let {		children,		class: className,		...restProps	}: HTMLAttributes<HTMLDivElement> & { children: Snippet } = $props();</script><div	data-slot="chat"	class={cn(		'flex min-h-0 flex-col overflow-hidden rounded-xl border border-border bg-background',		className	)}	{...restProps}>	{@render children()}</div>
chat-messages.svelte
<script lang="ts">	import type { Snippet } from 'svelte';	import type { HTMLAttributes } from 'svelte/elements';	import { cn } from '$lib/utils.js';	let {		children,		class: className,		role = 'log',		'aria-live': ariaLive = 'polite',		'aria-relevant': ariaRelevant = 'additions text',		...restProps	}: HTMLAttributes<HTMLDivElement> & { children: Snippet } = $props();</script><div	data-slot="chat-messages"	{role}	aria-live={ariaLive}	aria-relevant={ariaRelevant}	class={cn('flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto p-4', className)}	{...restProps}>	{@render children()}</div>
chat-message.svelte
<script lang="ts">	import type { Snippet } from 'svelte';	import type { HTMLAttributes } from 'svelte/elements';	import { cn } from '$lib/utils.js';	export type ChatSide = 'left' | 'right';	let {		children,		side = 'right',		class: className,		...restProps	}: HTMLAttributes<HTMLDivElement> & { children: Snippet; side?: ChatSide } = $props();</script><div	data-slot="chat-message"	data-side={side}	class={cn(		'group/message flex w-full items-end gap-2',		side === 'left' ? 'justify-start' : 'justify-end',		className	)}	{...restProps}>	{@render children()}</div>
chat-content.svelte
<script lang="ts">	import type { Snippet } from 'svelte';	import type { HTMLAttributes } from 'svelte/elements';	import { cn } from '$lib/utils.js';	let {		children,		class: className,		...restProps	}: HTMLAttributes<HTMLDivElement> & { children: Snippet } = $props();</script><div	data-slot="chat-content"	class={cn(		'max-w-[80%] rounded-2xl bg-muted px-4 py-2.5 text-sm leading-relaxed wrap-break-word text-foreground shadow-xs',		'group-data-[side=left]/message:rounded-bl-sm',		'group-data-[side=right]/message:rounded-br-sm group-data-[side=right]/message:bg-primary group-data-[side=right]/message:text-primary-foreground',		className	)}	{...restProps}>	{@render children()}</div>
chat-composer.svelte
<script lang="ts">	import type {		HTMLButtonAttributes,		HTMLFormAttributes,		HTMLTextareaAttributes	} from 'svelte/elements';	import { cn } from '$lib/utils.js';	type ComposerProps = Omit<HTMLFormAttributes, 'onsubmit'> & {		value?: string;		onSend?: (message: string) => void | Promise<void>;		placeholder?: string;		label?: string;		sendLabel?: string;		disabled?: boolean;		clearOnSend?: boolean;		submitOnEnter?: boolean;		rows?: number;		class?: string;		textareaClass?: string;		buttonClass?: string;		textareaProps?: Omit<			HTMLTextareaAttributes,			'value' | 'class' | 'disabled' | 'placeholder' | 'rows' | 'onkeydown'		>;		buttonProps?: Omit<HTMLButtonAttributes, 'type' | 'class' | 'disabled'>;	};	let {		value = $bindable(''),		onSend,		placeholder = 'Type a message…',		label = 'Message',		sendLabel = 'Send',		disabled = false,		clearOnSend = true,		submitOnEnter = true,		rows = 1,		class: className,		textareaClass,		buttonClass,		textareaProps,		buttonProps,		...restProps	}: ComposerProps = $props();	const inputId = $props.id();	let submitting = $state(false);	const cannotSend = $derived(disabled || submitting || value.trim().length === 0);	async function send(event: SubmitEvent) {		event.preventDefault();		const message = value.trim();		if (!message || disabled || submitting) return;		submitting = true;		try {			await onSend?.(message);			if (clearOnSend) value = '';		} finally {			submitting = false;		}	}	function handleKeydown(event: KeyboardEvent) {		if (			!submitOnEnter ||			event.key !== 'Enter' ||			event.shiftKey ||			event.isComposing ||			disabled ||			submitting		) {			return;		}		event.preventDefault();		(event.currentTarget as HTMLTextAreaElement).form?.requestSubmit();	}</script><form	{...restProps}	data-slot="chat-composer"	class={cn('flex items-end gap-2 border-t border-border bg-background p-3', className)}	onsubmit={send}>	<label class="sr-only" for={inputId}>{label}</label>	<textarea		{...textareaProps}		id={inputId}		data-slot="chat-input"		bind:value		{rows}		{placeholder}		disabled={disabled || submitting}		onkeydown={handleKeydown}		class={cn(			'max-h-40 min-h-10 flex-1 resize-none rounded-lg border border-input bg-background px-3 py-2 text-sm text-foreground shadow-xs outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50',			textareaClass		)}></textarea>	<button		{...buttonProps}		type="submit"		data-slot="chat-send"		disabled={cannotSend}		class={cn(			'inline-flex h-10 shrink-0 items-center justify-center rounded-lg bg-primary px-4 text-sm font-medium text-primary-foreground shadow-xs transition-colors hover:bg-primary/90 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50',			buttonClass		)}	>		{submitting ? 'Sending…' : sendLabel}	</button></form>
index.ts
import Root from './chat.svelte';import Messages from './chat-messages.svelte';import Message from './chat-message.svelte';import Content from './chat-content.svelte';import Composer from './chat-composer.svelte';export { Root, Messages, Message, Content, Composer };export {	Root as Chat,	Messages as ChatMessages,	Message as ChatMessage,	Content as ChatContent,	Composer as ChatComposer};export type { ChatSide } from './chat-message.svelte';