- Accordion
- Agent Thinking
- Alert
- Alert Dialog
- Announcement
- Aspect Ratio
- Attachment
- Avatar
- Badge
- Breadcrumb
- Bubble
- Button
- Button Group
- Calendar
- Card
- Carousel
- Checkbox
- Checkbox Card
- Code Block
- Collapsible
- Color Picker
- Combobox
- Command
- Composer
- Composer Loader
- Context Menu
- Data Grid
- Data Table
- Date Picker
- Dialog
- Direction
- Drawer
- Dropdown Menu
- Empty
- Field
- File Upload
- Hover Card
- Input
- Input Group
- Input Otp
- Item
- Kbd
- Label
- Marker
- Menubar
- Message
- Message Scroller
- Meter
- Native Select
- Navigation Menu
- Number Field
- Pagination
- Period Switcher
- Popover
- Progress
- Questionnaire
- Radio Card
- Radio Group
- Resizable
- Reveal
- Scroll Area
- Search Field
- Select
- Separator
- Sheet
- Sidebar
- Skeleton
- Slider
- Social Button
- Sonner
- Spinner
- Status Dot
- Switch
- Switch Card
- Table
- Tabs
- Tag Group
- Textarea
- Theme Toggle
- Time Field
- Toggle
- Toggle Group
- Tooltip
- Trace
- Tree View
- Agent Creator 01
- Agent Limits Card 01
- Agent Progress 01
- AI Chat 01
- AI Image Generation 01
- AI Profile 01
- Alerts Card 01
- Auth Card 01
- Calendar 01
- Composer 01
- Composer Attachments 01
- Composer Panel 01
- Finance Dashboard 01
- Home Dashboard 01
- Marketing Dashboard 01
- Medical Profile 01
- Meeting Scheduler 01
- Multi Agent Chat 01
- Notification Center 01
- Patient Info Card 01
- Project Board 01
- Questionnaire 01
- Settings Modal 01
- Sidebar 01
- Stat Cards 01
- Task List 01
- Web Search 01
- Activity Rings Card 01
- Area Chart Card 01
- Bar List Card 01
- Combo Chart Card 01
- Contributions Card 01
- Earnings Chart Card 01
- Funnel Chart Card 01
- Heatmap Chart Card 01
- Line Chart Card 01
- Most Active Days Card 01
- Orders Chart Card 01
- Radar Chart Card 01
- Radial Chart Card 01
- Revenue Chart Card 01
- Sankey Chart Card 01
- Scatter Chart Card 01
- Sleep Score Card 01
- Stage Bars Card 01
- Steps Card 01
For the assembled version with menus and a status row, see Composer 01.
Installation
Set up the @cobalt registry first, as described in Installation, because Composer depends on this registry's input-group. Then add the component:
$ pnpm dlx shadcn@latest add @cobalt/composer
Composer uses your theme's semantic tokens. The preview above shows it with the Cobalt theme, which you can add separately:
$ pnpm dlx shadcn@latest add @cobalt/theme-cobalt
Usage
<Composer onSend={(message) => send(message)} working={isWorking}>
<ComposerThinking />
<ComposerShell>
<ComposerInput aria-label="Message" placeholder="Write a message..." />
<ComposerToolbar>
<ComposerSend />
</ComposerToolbar>
</ComposerShell>
<ComposerFooter>Press Enter to send</ComposerFooter>
</Composer>Behavior
onSendreceives the trimmed message when the form is submitted with the send button or Enter. Empty messages are ignored.- Sending clears the field. Use
valueandonValueChangeto control it yourself. - While
workingis true, sending is disabled,ComposerThinkingshows an elapsed timer, and the shell shows the loading band. - Put your own controls in
ComposerToolbar, usingInputGroupButtonwithshape="pill"andsize="icon".
Source
"use client";
import { RiArrowUpLine } from "@remixicon/react";
import { cn } from "cn";
import * as React from "react";
import { AgentThinking } from "@/components/ui/agent-thinking";
import { ComposerLoader } from "@/components/ui/composer-loader";
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@/components/ui/input-group";
interface ComposerContextValue {
setValue: (value: string) => void;
value: string;
working: boolean;
}
const ComposerContext = React.createContext<ComposerContextValue | null>(null);
function useComposer() {
const context = React.useContext(ComposerContext);
if (!context) {
throw new Error("Composer parts must be used inside <Composer>.");
}
return context;
}
function Composer({
className,
value: valueProp,
defaultValue = "",
onValueChange,
onSend,
working = false,
children,
...props
}: Omit<React.ComponentProps<"form">, "defaultValue" | "onSubmit"> & {
value?: string;
defaultValue?: string;
onValueChange?: (value: string) => void;
onSend?: (message: string) => void;
working?: boolean;
}) {
const [internalValue, setInternalValue] = React.useState(defaultValue);
const value = valueProp ?? internalValue;
const setValue = React.useCallback(
(next: string) => {
setInternalValue(next);
onValueChange?.(next);
},
[onValueChange]
);
const context = React.useMemo(
() => ({ setValue, value, working }),
[setValue, value, working]
);
return (
<ComposerContext.Provider value={context}>
<form
data-slot="composer"
data-state={working ? "working" : "idle"}
className={cn("group/composer flex w-full flex-col gap-2", className)}
onSubmit={(event) => {
event.preventDefault();
const message = value.trim();
if (!message || working) {
return;
}
onSend?.(message);
setValue("");
}}
{...props}
>
{children}
</form>
</ComposerContext.Provider>
);
}
/**
* The status line above the shell while the agent works: an `AgentThinking`
* row (indicator, shimmering label, elapsed timer). It mounts with `working`,
* so the timer restarts on every send.
*/
function ComposerThinking({
className,
children = "Working",
label,
...props
}: Omit<React.ComponentProps<typeof AgentThinking>, "label" | "children"> & {
children?: string;
label?: string;
}) {
const { working } = useComposer();
if (!working) {
return null;
}
return (
<AgentThinking
label={label ?? children}
className={cn(
"px-3 duration-300 animate-in fade-in-0 motion-reduce:animate-none",
className
)}
{...props}
/>
);
}
function ComposerShell({
className,
children,
shape = "pill",
...props
}: React.ComponentProps<typeof InputGroup>) {
const { working } = useComposer();
return (
<InputGroup
data-slot="composer-shell"
shape={shape}
className={cn(
// No blue focus ring: a neutral, subtle ring keeps keyboard focus visible.
"has-[[data-slot=input-group-control]:focus-visible]:ring-1 has-[[data-slot=input-group-control]:focus-visible]:ring-foreground/15 group-data-[state=working]/composer:[&_[data-slot=button][data-size=icon]:not([type=submit])]:border-foreground/10 group-data-[state=working]/composer:[&_[data-slot=button][data-size=icon]:not([type=submit])]:bg-background/40 group-data-[state=working]/composer:[&_[data-slot=button][data-size=icon]:not([type=submit])]:backdrop-blur-md",
className
)}
{...props}
>
<ComposerLoader
active={working}
radius={shape === "pill" ? undefined : 10}
/>
{children}
</InputGroup>
);
}
function ComposerInput({
className,
onChange,
...props
}: Omit<React.ComponentProps<typeof InputGroupInput>, "value">) {
const { setValue, value } = useComposer();
return (
<InputGroupInput
autoComplete="off"
className={cn(
"relative z-1 h-full px-2 placeholder:text-muted-foreground/70 focus:placeholder:text-muted-foreground/70 md:text-sm",
className
)}
value={value}
onChange={(event) => {
onChange?.(event);
setValue(event.target.value);
}}
{...props}
/>
);
}
function ComposerToolbar({
className,
align = "inline-end",
...props
}: Omit<React.ComponentProps<typeof InputGroupAddon>, "align"> & {
align?: "inline-start" | "inline-end";
}) {
return (
<InputGroupAddon
data-slot="composer-toolbar"
align={align}
className={cn("relative z-1 gap-2 px-1 has-[>button]:mx-0", className)}
{...props}
/>
);
}
function ComposerSend({
children,
className,
disabled,
"aria-label": ariaLabel = "Send message",
...props
}: Omit<React.ComponentProps<typeof InputGroupButton>, "type">) {
const { working } = useComposer();
return (
<InputGroupButton
aria-label={ariaLabel}
className={cn(
"[&:disabled,&[aria-disabled=true]]:bg-[image:var(--gradient-button-primary-default,linear-gradient(180deg,oklch(from_var(--primary)_calc(l_+_0.08)_c_h),var(--primary)))] [&:disabled,&[aria-disabled=true]]:text-primary-foreground [&:disabled,&[aria-disabled=true]]:opacity-50",
className
)}
disabled={disabled || working}
shape="pill"
size="icon"
type="submit"
variant="default"
{...props}
>
{children ?? <RiArrowUpLine className="size-5" />}
</InputGroupButton>
);
}
function ComposerFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="composer-footer"
className={cn(
"flex min-w-0 flex-wrap items-center justify-between gap-2 px-0.5",
className
)}
{...props}
/>
);
}
export {
Composer,
ComposerFooter,
ComposerInput,
ComposerLoader,
ComposerSend,
ComposerShell,
ComposerThinking,
ComposerToolbar,
}; On this page