Refusal Message UI
How to say no without ending the conversation — the refusal, a plain-language reason, and one redirect the reader can click.
npx shadcn@latest add https://scrimui.dev/r/refusal-message.jsonAgent promptClaude Code · Cursor · any agent
Add the Refusal Message UI component from the Scrim UI registry to this project and use it in the configuration described below.
Refusal Message UI — How to say no without ending the conversation — the refusal, a plain-language reason, and one redirect the reader can click.
## 1. Install
```bash
npx shadcn@latest add https://scrimui.dev/r/refusal-message.json
```
This writes a single file to `components/ui/refusal-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/refusal-message to the same path by hand; nothing in the file depends on shadcn.
## 2. Use it
```tsx
import { RefusalMessage } from "@/components/ui/refusal-message";
<RefusalMessage
message="I can't help with gaining access to a network you don't own."
reason="This falls under unauthorized access — I can only help with networks you administer yourself."
suggestion="Ask about securing my own home network instead"
onSuggestion={() => {}}
/>
```
## 3. The configuration that matters
This is the component's "With redirect" state — The full pattern: what won't be done, why in plain terms, and the one pivot still on the table — as a button.
Every prop is at its default value. Keep the call site minimal — do not write out default values.
## 4. Wire the handlers
`onSuggestion` is a stub in the snippet above (`() => {}`). Connect it to this project's own state and data layer instead of shipping the empty function.
Reference: https://scrimui.dev/components/refusal-messageFollows the props below — change a control and the prompt changes with it, so an agent reproduces that configuration instead of the defaults.
I can't help with gaining access to a network you don't own.
Why: This falls under unauthorized access — I can only help with networks you administer yourself.
Presets
Props
Also takes onSuggestion.
Component source
Single-file React + Tailwind component. No dependencies — drop it into any project with Tailwind configured.
"use client";
import * as React from "react";
/**
* Say no like a colleague, not like a firewall.
*
* A bare "I can't help with that" is a dead end: the user learns nothing and
* the conversation is over. A useful refusal does three things, in order —
* states what won't be done, says why in the user's terms (never a policy
* clause number, which reads as hiding behind a rulebook), and offers the one
* pivot that is still on the table.
*
* **The redirect is the component.** `suggestion` is not decoration; it is
* the difference between a refusal and a rejection. When it is present it is
* a button, because making the user retype the safe version of their own
* request is friction added at exactly the moment they are most frustrated.
*
* **A refusal is not an error.** No red, no alert triangle — the system
* worked as intended. The surface stays neutral so the reader's eye goes to
* the words, not to alarm styling the situation has not earned.
*
* **Keep it in the thread.** The refusal is an assistant turn like any other;
* pulling it out into a modal or a banner breaks the conversation's record of
* what was asked and declined.
*/
export type RefusalMessageProps = {
/** The refusal itself, in one or two plain sentences. */
message: string;
/** The plain-language basis for the refusal — "why", not a policy citation. */
reason?: string;
/** The offered pivot. Rendered as an action, not a hint. */
suggestion?: string;
/** Fired when the reader accepts the pivot — typically sends it as a new prompt. */
onSuggestion?: () => void;
className?: string;
};
/* ------------------------------------------------------------------ */
/* Icons */
/* ------------------------------------------------------------------ */
function ShieldIcon() {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" width="16" height="16">
<path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" />
</svg>
);
}
function ArrowRightIcon() {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" width="13" height="13">
<path d="M5 12h14" />
<path d="m12 5 7 7-7 7" />
</svg>
);
}
/* ------------------------------------------------------------------ */
/* RefusalMessage */
/* ------------------------------------------------------------------ */
export function RefusalMessage({
message,
reason,
suggestion,
onSuggestion,
className = "",
}: RefusalMessageProps) {
return (
<div className={`flex items-start gap-3 rounded-2xl rounded-tl-md border border-zinc-200 bg-zinc-50 px-4 py-3 dark:border-zinc-800 dark:bg-zinc-900 ${className}`}>
<span className="mt-0.5 shrink-0 text-zinc-500 dark:text-zinc-400">
<ShieldIcon />
</span>
<div className="min-w-0 flex-1">
<p className="text-sm leading-6 text-zinc-900 dark:text-zinc-100">{message}</p>
{/* zinc-600 on the caption: it sits on the zinc-50 surface, where
zinc-500 measures under AA at 12px. */}
{reason && (
<p className="mt-1.5 text-[13px] leading-5 text-zinc-600 dark:text-zinc-400">
<span className="font-medium">Why: </span>
{reason}
</p>
)}
{suggestion && (
<button
type="button"
onClick={onSuggestion}
className="mt-2.5 inline-flex h-7 max-w-full items-center gap-1.5 rounded-lg border border-zinc-200 bg-white px-2.5 text-xs font-medium text-zinc-700 transition-colors hover:bg-zinc-100 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-200 dark:hover:bg-zinc-800"
>
<span className="truncate">{suggestion}</span>
<span className="shrink-0">
<ArrowRightIcon />
</span>
</button>
)}
</div>
</div>
);
}
When to use it
- State what won't be done, then why in the user's own terms — never a policy clause number, which reads as hiding behind a rulebook.
- Offer exactly one redirect and make it a button; retyping the safe version by hand is friction at the moment of maximum frustration.
- Keep the surface neutral. A refusal is the system working as intended, not an error — red alert styling it hasn't earned only inflames.
- Keep the refusal in the message thread so the record of what was asked and declined survives.
What breaks in production
- A bare 'I can't help with that' with no reason and no pivot — the user learns nothing and the conversation is over.
- Apologising three times before getting to the point; the refusal should be the first sentence, not the third.
- Refusing the whole request when only part of it is over the line — answer the safe half, decline the rest.
Related Components
What to show when the content filter fires — a stopped stream that keeps its partial text, or a blocked prompt, with a false-positive path.
An answer that says how sure it is — a warning badge only when there is something to warn about, and a hedge that names what to verify.
The message input at the heart of an AI chat app — file attachments, model picker, tool toggles, voice, and a send button that turns into stop.
A chat input that accepts file uploads — attachment chips with upload progress, type icons, and one-click removal.