Skip to content
Scrim UI

User Message UI

The user's chat bubble — right-aligned, with hover actions to copy, edit the prompt, or regenerate the answer.

usermessagechatbubbleedit

Preview

You
Give me a table comparing the three streaming approaches — time to first token, perceived latency and implementation cost.

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-message

Follows 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-message

Component 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
user-message.tsx
"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