Browse all components
User Message UI
The user's chat bubble — right-aligned, with hover actions to copy, edit the prompt, or regenerate the answer.
Preview
Presets
Props
Also takes onCopy, onEdit, onRegenerate.
Agent promptClaude Code · Cursor · any agent
Add the User Message UI component from the Scrim UI registry to this project and use it in the configuration described below.
User Message UI — The user's chat bubble — right-aligned, with hover actions to copy, edit the prompt, or regenerate the answer.
## 1. Install
```bash
npx shadcn@latest add @scrimui/user-message
```
This writes a single file to `components/ui/user-message.tsx`. It is plain React + Tailwind with no runtime dependencies — no Radix, no CVA, nothing to add to package.json. If this project does not use the shadcn CLI, copy the source from https://scrimui.dev/components/user-message to the same path by hand; nothing in the file depends on shadcn.
## 2. Use it
```tsx
import { UserMessage } from "@/components/ui/user-message";
<UserMessage
text="Give me a table comparing the three streaming approaches — time to first token, perceived latency and implementation cost."
showActions
onCopy={() => {}}
onEdit={() => {}}
onRegenerate={() => {}}
/>
```
## 3. The configuration that matters
This is the component's "Default" state — Right-aligned bubble with the full action row.
Every prop is at its default value. Keep the call site minimal — do not write out default values.
## 4. Wire the handlers
`onCopy`, `onEdit`, `onRegenerate` are stubs in the snippet above (`() => {}`). Connect them to this project's own state and data layer instead of shipping the empty functions.
Reference: https://scrimui.dev/components/user-messageFollows the props below — change a control and the prompt changes with it, so an agent reproduces that configuration instead of the defaults.
Install
npx shadcn@latest add @scrimui/user-messageComponent source
React + Tailwind component using shadcn/ui (badge, button). The install command adds these dependencies automatically.
When copying source manually, install the required primitives first:
npx shadcn@latest add badge button"use client";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import * as React from "react";
/* ------------------------------------------------------------------ */
/* Types */
/* ------------------------------------------------------------------ */
export type UserMessageProps = {
text: string;
/** Shows an "Edited" chip next to the sender name. */
edited?: boolean;
showActions?: boolean;
onCopy?: () => void;
onEdit?: () => void;
onRegenerate?: () => void;
avatar?: React.ReactNode;
className?: string;
};
/* ------------------------------------------------------------------ */
/* Icons */
/* ------------------------------------------------------------------ */
function CopyIcon() {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" width="13" height="13">
<rect x="9" y="9" width="13" height="13" rx="2" />
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
</svg>
);
}
function EditIcon() {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" width="13" height="13">
<path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" />
</svg>
);
}
function RegenerateIcon() {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" width="13" height="13">
<path d="M21 12a9 9 0 1 1-2.64-6.36L21 8" />
<path d="M21 3v5h-5" />
</svg>
);
}
/* ------------------------------------------------------------------ */
/* UserMessage */
/* ------------------------------------------------------------------ */
export function UserMessage({
text,
edited = false,
showActions = true,
onCopy,
onEdit,
onRegenerate,
avatar,
className = "",
}: UserMessageProps) {
const [copied, setCopied] = React.useState(false);
const copy = () => {
onCopy?.();
setCopied(true);
window.setTimeout(() => setCopied(false), 1500);
};
return (
<div className={`flex items-start justify-end gap-3 ${className}`}>
<div className="min-w-0 max-w-[85%]">
<div className="flex flex-wrap items-center justify-end gap-2">
<span className="text-sm font-medium">You</span>
{edited && (
<Badge variant="secondary" className="rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground">
Edited
</Badge>
)}
</div>
<div className="mt-1.5 whitespace-pre-wrap rounded-xl bg-muted px-4 py-3 text-sm leading-6 text-foreground">
{text}
</div>
{showActions && (onCopy || onEdit || onRegenerate) && (
<div className="mt-2 flex items-center justify-end gap-1">
{onCopy && (
<Button variant="ghost" size="sm"
type="button"
onClick={copy}
className="inline-flex h-8 items-center gap-1.5 rounded-md px-2 text-xs text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
>
<CopyIcon />
{copied ? "Copied" : "Copy"}
</Button>
)}
{onEdit && (
<Button variant="ghost" size="sm"
type="button"
onClick={onEdit}
className="inline-flex h-8 items-center gap-1.5 rounded-md px-2 text-xs text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
>
<EditIcon />
Edit
</Button>
)}
{onRegenerate && (
<Button variant="ghost" size="sm"
type="button"
onClick={onRegenerate}
className="inline-flex h-8 items-center gap-1.5 rounded-md px-2 text-xs text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
>
<RegenerateIcon />
Regenerate
</Button>
)}
</div>
)}
</div>
{avatar && <div className="shrink-0">{avatar}</div>}
</div>
);
}
When to use it
- Right-align the user's turn and left-align the assistant's — the strongest possible signal of who said what in a conversation.
- Show actions only on the focused or hovered message to keep the transcript calm; pin them always for short-lived transcripts.
- Use the 'Edited' chip honestly — it changes how the model should treat the turn and how collaborators read the thread.
- Put copy and regenerate on the user turn too: users re-run or refine their own prompt as often as they regenerate an answer.
What breaks in production
- Making the bubble the same color as the assistant's — the fill contrast is the primary sender signal.
- Clipping long prompts with an ellipsis in the transcript; let them wrap and scroll naturally.
- Showing every action on every message at once, which turns a chat log into a toolbar wall.
Related Components
Render an AI reply token by token like ChatGPT's typing effect — blinking cursor, stop button, and no layout jump as the text grows.
The action row under an AI reply — copy, regenerate, share and thumbs up/down feedback, revealed on hover.
What to show when a generation fails — a plain-English reason, a retry button, and a countdown for rate limits.
A rendered markdown reply — syntax-highlighted code blocks with copy buttons, plus tables, lists and safe links.