Refined UI

An opinionated, customizable superset of shadcn/ui

AgentsPrimitivesSkillsSkins
Integration
Guides
  • Overview
  • Get started
  • Shadcn compatibility
  • Architecture
  • Agent surface
Agents
  • Action Bar
  • Audio Recorder
  • Audio VisualizerBetaBeta — the props contract is close to final, but small breaking changes can still land.
  • Chat Input
  • Chat Input Attachment
  • Chat Message
  • Chat Scene
  • Code Block Editor
  • Dynamic NotificationExpExperimental — the contract and the rendering can change without notice. Own the installed copy before shipping it.
  • Environment Variables
  • Inline Attachment
  • Markdown Block
  • Task List
  • Thread Rail
  • Tool Call
  • User Ask
Blocks
  • Chat
  • Theme toggle
  1. Agents
  2. ChatInput
Agents

ChatInput

Prompt composer with controlled text, submit state, and trigger-menu support.

Composition

The preferred shape for composing the installed agent from its exported parts.

Parts

Exported compound parts from the installed source.

ChatInput
├── ChatInputShell
├── ChatInputAccent
├── ChatInputTextarea
├── ChatInputToolbar
├── ChatInputTools
├── ChatInputFooter
└── ChatInputSubmit

This agent installs from the chat registry. Install the bundle with the command above, or inspect the source below.

Registry command
npx shadcn@latest add http://127.0.0.1:3000/r/refined/chat.json
See registry manifest

Usage

import {
ChatInput,
ChatInputShell,
ChatInputSubmit,
ChatInputTextarea,
ChatInputToolbar,
ChatInputTools,
} from "@/components/refined-ui/chat-input";
​
type MastraTextMessageInput = {
role: "user";
content: {
format: 2;
parts: [{ type: "text"; text: string }];
};
};
​
function createMastraTextMessage(text: string): MastraTextMessageInput {
return {
role: "user",
content: {
format: 2,
parts: [{ type: "text", text }],
},
};
}
​
export function Example({ send }: { send: (message: MastraTextMessageInput) => void | Promise<void> }) {
return (
<ChatInput
onSubmit={async ({ value, clear }) => {
await send(createMastraTextMessage(value));
clear();
}}
>
<ChatInputShell>
<ChatInputTextarea placeholder="Ask anything..." />
<ChatInputToolbar>
<ChatInputTools>Mastra</ChatInputTools>
<ChatInputSubmit>Send</ChatInputSubmit>
</ChatInputToolbar>
</ChatInputShell>
</ChatInput>
);
}
​

Installed dependencies

Installed with this agent because the visible component depends on them; they are support files, not separate product surfaces.

Behavior hooksrc/registry/hooks/use-chat-input.tsHook
Editor schemasrc/registry/sources/refined/chat-input-editor/schema.tsSupport
Extension contractsrc/registry/sources/refined/chat-input-editor/types.tsSupport
Exit-animation ghostsrc/registry/sources/refined/chat-input-editor/ghost.tsSupport
Editor motion (blur choreography)src/registry/sources/refined/chat-input-editor.cssSupport

Raw code

Primary installed agent source

Component
src/registry/sources/refined/chat-input.tsx
"use client";
​
import type { ChangeEvent, ComponentProps } from "react";
import { createContext, useContext } from "react";
​
import type { ChatInputProps } from "@/components/refined-ui/contracts";
import { useChatInput } from "@/components/refined-ui/hooks/use-chat-input";
import { cn } from "@/components/refined-ui/lib/cn";
import { hasSkinAdornment, skinAdornment, skinSlot } from "@/components/refined-ui/skin";
import { Button } from "@/components/refined-ui/ui/button";
​
type ChatInputContextValue = ReturnType<typeof useChatInput>;
​
const ChatInputContext = createContext<ChatInputContextValue | null>(null);
​
// Exported so opt-in editor extras (chat-input-editor.tsx) share this context w/o pulling ProseMirror deps into base file.
export function useChatInputContext() {
const context = useContext(ChatInputContext);
if (!context) throw new Error("ChatInput compound components must be rendered inside <ChatInput>.");
return context;
}
​
export function ChatInput({
value,
defaultValue,
onValueChange,
onSubmit,
state = "idle",
density = "comfortable",
disabled = false,
className,
children,
...props
}: ChatInputProps) {
const input = useChatInput({
value,
defaultValue,
onValueChange,
onSubmit,
state,
density,
disabled,
trackSends: hasSkinAdornment("chat-input:send-layer"),
});
const sendLayer = skinAdornment("chat-input:send-layer", { sendCount: input.sendCount });
​
return (
<ChatInputContext.Provider value={input}>
<form
data-ui="agent"
data-component="chat-input"
data-state={state}
data-density={density}
onSubmit={input.handleSubmit}
className={cn(
// sticky already makes the form the containing block + stacking context the send-layer anchor relies on
"sticky bottom-0 w-full bg-background/80 px-2 pb-2 pt-1 backdrop-blur supports-[backdrop-filter]:bg-background/70",
skinSlot("chat-input", {}),
className,
)}
{...props}
>
{/* send-layer anchor: the component owns this position contract (backdrop behind the shell, hit-test/a11y/paint
contained); the skin only supplies visuals. Zero DOM when no skin fills the anchor. */}
{sendLayer !== undefined && sendLayer !== null ? (
<div aria-hidden="true" className="pointer-events-none absolute inset-0 -z-10 overflow-clip [contain:paint]">
{sendLayer}
</div>
) : null}
{children}
</form>
</ChatInputContext.Provider>
);
}
​
export type ChatInputShellProps = ComponentProps<"div">;
​
export function ChatInputShell({ className, ...props }: ChatInputShellProps) {
const input = useChatInputContext();
​
return (
<div
data-ui="agent"
data-slot="chat-input-shell"
data-state={input.state}
className={cn(
"relative overflow-hidden rounded-field border bg-card/78 shadow-sm ring-1 ring-foreground/4 transition-[box-shadow,filter,translate] duration-[var(--duration-base)] ease-[var(--ease-emphasized)]",
"data-[state=submitting]:-translate-y-0.5 data-[state=submitting]:saturate-[1.02] data-[state=submitting]:shadow-md",
skinSlot("chat-input-shell", {}),
className,
)}
{...props}
/>
);
}
​
export type ChatInputAccentProps = ComponentProps<"div">;
​
export function ChatInputAccent({ className, ...props }: ChatInputAccentProps) {
return (
<div
aria-hidden="true"
className={cn(
"pointer-events-none absolute inset-x-5 top-0 h-px bg-gradient-to-r from-transparent via-foreground/20 to-transparent",
skinSlot("chat-input-accent", {}),
className,
)}
{...props}
/>
);
}
​
export type ChatInputTextareaProps = ComponentProps<"textarea">;
​
export function ChatInputTextarea({ className, rows, disabled, onChange, ...props }: ChatInputTextareaProps) {
const input = useChatInputContext();
​
function handleChange(event: ChangeEvent<HTMLTextAreaElement>) {
onChange?.(event);
if (!event.defaultPrevented) input.setValue(event.currentTarget.value);
}
​
return (
<textarea
{...props}
data-ui="agent"
data-component="chat-input-textarea"
aria-label="Message"
value={input.value}
onChange={handleChange}
disabled={disabled ?? input.isDisabled}
rows={rows ?? input.rows}
className={cn(
// CSS-first auto-grow via field-sizing-content (bounded min/max-h, no ref/ResizeObserver/scrollHeight); rows = fallback where unsupported.
"field-sizing-content min-h-16 max-h-[40dvh] w-full resize-none bg-transparent px-[var(--padding-x)] py-[var(--padding-y)] text-sm leading-6 outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-60",
skinSlot("chat-input-textarea", {}),
className,
)}
/>
);
}
​
export type ChatInputToolbarProps = ComponentProps<"div">;
​
export function ChatInputToolbar({ className, ...props }: ChatInputToolbarProps) {
return (
<div
className={cn("flex min-h-10 items-center justify-between gap-2 px-2.5 pb-2", skinSlot("chat-input-toolbar", {}), className)}
{...props}
/>
);
}
​
export type ChatInputToolsProps = ComponentProps<"div">;
​
export function ChatInputTools({ className, ...props }: ChatInputToolsProps) {
return (
<div
className={cn("flex min-w-0 items-center gap-1.5 text-muted-foreground", skinSlot("chat-input-tools", {}), className)}
{...props}
/>
);
}
​
export type ChatInputFooterProps = ComponentProps<"div">;
​
export function ChatInputFooter({ className, ...props }: ChatInputFooterProps) {
return <div className={cn("px-3 pb-2 text-meta text-muted-foreground", skinSlot("chat-input-footer", {}), className)} {...props} />;
}
​
export type ChatInputSubmitProps = ComponentProps<typeof Button>;
​
export function ChatInputSubmit({ className, disabled, children = "Send", ...props }: ChatInputSubmitProps) {
const input = useChatInputContext();
​
return (
<Button
data-ui="agent"
data-component="chat-input-submit"
type="submit"
variant="solid"
size="xs"
disabled={disabled ?? !input.canSubmit}
className={cn(
"h-7",
input.canSubmit
? "bg-primary text-primary-foreground shadow-sm hover:bg-primary/90"
: "cursor-not-allowed bg-muted text-muted-foreground",
skinSlot("chat-input-submit", {}),
className,
)}
{...props}
>
{children}
</Button>
);
}
​

Available extensions

Optional, separately installed items this surface can host — not part of the component's bundle.

SendAuroraAnchored

Anchored ChatInput extension: a blurred aurora backdrop that sweeps up once per sent message — activated from skin.config via the chat-input:send-layer anchor.

Install
npx shadcn@latest add http://127.0.0.1:3000/r/refined/send-aurora.json
// skin.config.tsx (import the "use client" extension; the config itself stays RSC-pure)
import { SendAurora } from "@/components/refined-ui/extensions/send-aurora";
​
export const skin: RefinedSkin = {
id: "my-brand",
adornments: {
"chat-input:send-layer": (ctx) => <SendAurora sendCount={ctx.sendCount} />,
},
};

Primitives

This agent composes these library primitives, installed under components/refined-ui/ui/*.

Buttoncomponents/refined-ui/ui/button.tsxLibrary slotScroll areacomponents/refined-ui/ui/scroll-area.tsxLibrary slotTrigger Menucomponents/refined-ui/ui/trigger-menu.tsxLibrary slot

On this page

  • Preview
  • Composition
  • Installation
  • Usage
  • Dependencies
  • Source
  • Extensions
  • Primitives
MastraMastra agent
Last submitted: Summarize the latest deployment notes.