mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-07-01 00:24:20 +00:00
feat(skill): add chat component guidance to shadcn skill (#11048)
Add MessageScroller, Message, Bubble, Attachment, and Marker guidance, driven by a baseline gap analysis: fresh agents hand-rolled all five primitives, so this adds a rules/chat.md, discovery hooks in SKILL.md, two evals, and supporting composition/styling rules. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: shadcn
|
||||
description: Manages shadcn components and projects — adding, searching, fixing, debugging, styling, and composing UI. Provides project context, component docs, and usage examples. Applies when working with shadcn/ui, component registries, presets, --preset codes, or any project with a components.json file. Also triggers for "shadcn init", "create an app with --preset", or "switch to --preset".
|
||||
description: Manages shadcn components and projects — adding, searching, fixing, debugging, styling, and composing UI, including chat interfaces. Provides project context, component docs, and usage examples. Applies when working with shadcn/ui, component registries, presets, --preset codes, or any project with a components.json file. Also triggers for "shadcn init", "create an app with --preset", or "switch to --preset".
|
||||
user-invocable: false
|
||||
allowed-tools: Bash(npx shadcn@latest *), Bash(pnpm dlx shadcn@latest *), Bash(bunx --bun shadcn@latest *)
|
||||
---
|
||||
@@ -75,6 +75,12 @@ These rules are **always enforced**. Each links to a file with Incorrect/Correct
|
||||
- **No sizing classes on icons inside components.** Components handle icon sizing via CSS. No `size-4` or `w-4 h-4`.
|
||||
- **Pass icons as objects, not string keys.** `icon={CheckIcon}`, not a string lookup.
|
||||
|
||||
### Chat & Messaging → [chat.md](./rules/chat.md)
|
||||
|
||||
- **Chat UI composes the chat primitives.** Conversations use `MessageScroller`, rows use `Message`, surfaces use `Bubble`. Never hand-rolled bubble `div`s or a raw scroll container.
|
||||
- **`MessageScroller` owns scroll behavior.** Streaming follow, anchoring, and jump-to-latest (`MessageScrollerButton`) are built in. Don't write a `useStickToBottom`/`ResizeObserver` hook.
|
||||
- **Attachments use `Attachment`; system notes and dividers use `Marker`.** Not `Item` cards or `Separator` + a label.
|
||||
|
||||
### CLI
|
||||
|
||||
- **Never decode preset codes or build preset URLs manually.** Use `npx shadcn@latest preset decode <code>`, `preset url <code>`, or `preset open <code>`. For project-aware preset detection, use `npx shadcn@latest preset resolve`.
|
||||
@@ -136,6 +142,7 @@ These are the most common patterns that differentiate correct shadcn/ui code. Fo
|
||||
| Empty states | `Empty` |
|
||||
| Menus | `DropdownMenu`, `ContextMenu`, `Menubar` |
|
||||
| Tooltips/info | `Tooltip`, `HoverCard`, `Popover` |
|
||||
| Chat / conversation UI | `MessageScroller`, `Message`, `Bubble`, `Attachment`, `Marker` |
|
||||
|
||||
## Key Fields
|
||||
|
||||
@@ -259,6 +266,7 @@ npx shadcn@latest view owner/repo/item
|
||||
|
||||
- [rules/forms.md](./rules/forms.md) — FieldGroup, Field, InputGroup, ToggleGroup, FieldSet, validation states
|
||||
- [rules/composition.md](./rules/composition.md) — Groups, overlays, Card, Tabs, Avatar, Alert, Empty, Toast, Separator, Skeleton, Badge, Button loading
|
||||
- [rules/chat.md](./rules/chat.md) — MessageScroller, Message, Bubble, Attachment, Marker; streaming, anchoring, jump-to-latest
|
||||
- [rules/icons.md](./rules/icons.md) — data-icon, icon sizing, passing icons as objects
|
||||
- [rules/styling.md](./rules/styling.md) — Semantic colors, variants, className, spacing, size, truncate, dark mode, cn(), z-index
|
||||
- [rules/base-vs-radix.md](./rules/base-vs-radix.md) — asChild vs render, Select, ToggleGroup, Slider, Accordion
|
||||
|
||||
@@ -42,6 +42,36 @@
|
||||
"Uses gap-* instead of space-y-* or space-x-* for spacing",
|
||||
"Uses size-* when width and height are equal instead of separate w-* h-*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"prompt": "I'm building a Next.js app with shadcn/ui (base-nova preset, lucide icons). Build a chat conversation view: a scrollable thread of messages from two different people, each with an avatar, sender name, timestamp, and message bubble. A couple of messages include an image attachment and a PDF file attachment, and there's a 'Today' divider separating the days.",
|
||||
"expected_output": "A React component composing MessageScroller, Message, Bubble, Attachment, and Marker from the registry instead of hand-rolled bubble/divider/attachment markup.",
|
||||
"files": [],
|
||||
"expectations": [
|
||||
"Uses MessageScroller (MessageScrollerProvider, MessageScrollerViewport, MessageScrollerContent, MessageScrollerItem) for the scrollable thread instead of a raw overflow-y-auto div or ScrollArea",
|
||||
"Wraps each row in MessageScrollerItem inside MessageScrollerContent",
|
||||
"Uses Message with MessageAvatar/MessageContent/MessageHeader for row layout instead of custom flex divs",
|
||||
"Uses Bubble + BubbleContent for the message surface instead of a styled div with bg-muted/bg-primary",
|
||||
"Uses Attachment (AttachmentMedia, AttachmentContent, AttachmentTitle, AttachmentDescription) for the file and image attachments instead of Item or a custom card",
|
||||
"Uses Marker (variant=\"separator\") for the 'Today' divider instead of Separator plus a centered label",
|
||||
"Uses semantic color tokens and gap-* spacing; no raw colors like bg-emerald-500 and no space-y-*",
|
||||
"Includes \"use client\" when the component uses state or event handlers (isRSC)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"prompt": "Using shadcn/ui (base-nova preset, lucide icons), build a streaming AI chat UI. The assistant's reply streams in while it generates, the view auto-scrolls to follow the latest content but stops following if the user scrolls up to read earlier messages, a 'jump to latest' button appears when the user has scrolled away from the bottom, and a subtle 'thinking…' shimmer shows while the model is generating.",
|
||||
"expected_output": "A React component that delegates scroll/anchor behavior to MessageScroller and uses MessageScrollerButton for jump-to-latest and the shimmer utility for the thinking indicator — no hand-rolled scroll logic or custom shimmer keyframes.",
|
||||
"files": [],
|
||||
"expectations": [
|
||||
"Uses MessageScroller with MessageScrollerProvider (autoScroll) and scrollAnchor on message items for the stick-to-bottom/follow behavior instead of a custom useStickToBottom hook or ResizeObserver/scrollTop wiring",
|
||||
"Uses MessageScrollerButton for the jump-to-latest control instead of a hand-built conditional button driven by manual scroll-position state",
|
||||
"Uses the shimmer utility class for the 'thinking…' indicator instead of a custom @keyframes or bg-clip-text gradient animation",
|
||||
"Wraps each message row in MessageScrollerItem inside MessageScrollerContent",
|
||||
"Uses Message + Bubble + BubbleContent for the conversation rows instead of hand-rolled bubble divs",
|
||||
"Uses semantic color tokens and gap-* spacing; includes \"use client\" (isRSC)"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
224
skills/shadcn/rules/chat.md
Normal file
224
skills/shadcn/rules/chat.md
Normal file
@@ -0,0 +1,224 @@
|
||||
# Chat & Messaging
|
||||
|
||||
Components for conversation and chat UI. Compose these instead of hand-rolling
|
||||
bubbles, scroll containers, dividers, or attachment cards.
|
||||
|
||||
Install: `npx shadcn@latest add message-scroller message bubble attachment marker`
|
||||
|
||||
The same component names and props ship for both `base` and `radix`; only
|
||||
composition differs (`render` vs `asChild`). See [base-vs-radix.md](./base-vs-radix.md).
|
||||
|
||||
## Contents
|
||||
|
||||
- Scrollable threads use MessageScroller
|
||||
- Message rows use Message
|
||||
- Message surfaces use Bubble
|
||||
- Attachments use Attachment
|
||||
- System notes and dividers use Marker
|
||||
- Streaming, anchoring, and jump-to-latest are built in
|
||||
- Escape hatch: the scroller hooks
|
||||
|
||||
---
|
||||
|
||||
## Scrollable threads use MessageScroller
|
||||
|
||||
A conversation that scrolls, follows new messages, restores position, or jumps
|
||||
to a message uses `MessageScroller`. Don't build a raw overflow container with
|
||||
manual scroll wiring, and don't reach for `ScrollArea`.
|
||||
|
||||
The parts nest in a fixed order. Every direct child of the content is wrapped in
|
||||
a `MessageScrollerItem` so the scroller can measure, anchor, preserve position,
|
||||
track visibility, and jump to it. `MessageScrollerButton` sits inside
|
||||
`MessageScroller`, after the viewport.
|
||||
|
||||
**Incorrect:**
|
||||
|
||||
```tsx
|
||||
// Hand-rolled scroll container with manual stick-to-bottom logic.
|
||||
<div ref={scrollRef} onScroll={handleScroll} className="flex-1 overflow-y-auto">
|
||||
<div className="flex flex-col gap-6 p-4">
|
||||
{messages.map((m) => (
|
||||
<ChatMessage key={m.id} message={m} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Correct:**
|
||||
|
||||
```tsx
|
||||
<MessageScrollerProvider autoScroll>
|
||||
<MessageScroller>
|
||||
<MessageScrollerViewport>
|
||||
<MessageScrollerContent>
|
||||
{messages.map((message) => (
|
||||
<MessageScrollerItem
|
||||
key={message.id}
|
||||
messageId={message.id}
|
||||
scrollAnchor={message.role === "user"}
|
||||
>
|
||||
<Message align={message.role === "user" ? "end" : "start"}>
|
||||
{/* ...message content... */}
|
||||
</Message>
|
||||
</MessageScrollerItem>
|
||||
))}
|
||||
</MessageScrollerContent>
|
||||
</MessageScrollerViewport>
|
||||
<MessageScrollerButton />
|
||||
</MessageScroller>
|
||||
</MessageScrollerProvider>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Message rows use Message
|
||||
|
||||
`Message` lays out a single row: avatar, header, content, footer, with
|
||||
alignment. Group consecutive rows from one sender with `MessageGroup`. Don't
|
||||
rebuild the row from flex divs.
|
||||
|
||||
`align="end"` is the current user's side; `align="start"` is everyone else.
|
||||
|
||||
```tsx
|
||||
<Message align="start">
|
||||
<MessageAvatar>
|
||||
<Avatar>
|
||||
<AvatarImage src={sender.avatar} alt={sender.name} />
|
||||
<AvatarFallback>{initials}</AvatarFallback>
|
||||
</Avatar>
|
||||
</MessageAvatar>
|
||||
<MessageContent>
|
||||
<MessageHeader>{sender.name}</MessageHeader>
|
||||
<Bubble>
|
||||
<BubbleContent>{text}</BubbleContent>
|
||||
</Bubble>
|
||||
<MessageFooter>{time}</MessageFooter>
|
||||
</MessageContent>
|
||||
</Message>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Message surfaces use Bubble
|
||||
|
||||
The colored message surface is `Bubble` + `BubbleContent`, never a styled `div`
|
||||
with `bg-muted` / `bg-primary` and hand-managed corners.
|
||||
|
||||
- `variant`: `default`, `secondary`, `muted`, `tinted`, `outline`, `ghost`, `destructive`.
|
||||
- `align`: `start` or `end` (matches the `Message` side).
|
||||
|
||||
`BubbleReactions` renders the reaction cluster. `side` (`top` | `bottom`) and
|
||||
`align` (`start` | `end`) position it against the bubble. Don't lay reactions out
|
||||
with absolutely-positioned `Badge`s.
|
||||
|
||||
**Incorrect:**
|
||||
|
||||
```tsx
|
||||
<div className="w-fit rounded-2xl bg-primary px-3 py-2 text-primary-foreground">
|
||||
{text}
|
||||
</div>
|
||||
```
|
||||
|
||||
**Correct:**
|
||||
|
||||
```tsx
|
||||
<Bubble variant="default" align="end">
|
||||
<BubbleContent>{text}</BubbleContent>
|
||||
<BubbleReactions side="bottom" align="end">
|
||||
<Badge variant="secondary">👍 2</Badge>
|
||||
</BubbleReactions>
|
||||
</Bubble>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Attachments use Attachment
|
||||
|
||||
File and image attachments use `Attachment`, not `Item` or a custom card. It
|
||||
carries upload state, so wire `state` to the real status rather than rendering a
|
||||
separate spinner.
|
||||
|
||||
- `state`: `idle`, `uploading`, `processing`, `error`, `done`. `uploading` and
|
||||
`processing` apply the `shimmer` animation to the title automatically.
|
||||
- `size`: `default`, `sm`, `xs`. `orientation`: `horizontal`, `vertical`.
|
||||
- Use `AttachmentGroup` to lay out several attachments in a scrolling row.
|
||||
|
||||
```tsx
|
||||
<Attachment state="done">
|
||||
<AttachmentMedia variant="icon">
|
||||
<FileTextIcon />
|
||||
</AttachmentMedia>
|
||||
<AttachmentContent>
|
||||
<AttachmentTitle>homepage-feedback.pdf</AttachmentTitle>
|
||||
<AttachmentDescription>PDF · 2.4 MB</AttachmentDescription>
|
||||
</AttachmentContent>
|
||||
<AttachmentActions>
|
||||
<AttachmentAction>
|
||||
<DownloadIcon />
|
||||
</AttachmentAction>
|
||||
</AttachmentActions>
|
||||
</Attachment>
|
||||
```
|
||||
|
||||
For an image, use `<AttachmentMedia variant="image">` with an `img` child.
|
||||
|
||||
---
|
||||
|
||||
## System notes and dividers use Marker
|
||||
|
||||
Status lines ("Sarah joined the conversation"), date dividers ("Today"), and
|
||||
labeled separators are `Marker`, not a `Separator` plus a centered span.
|
||||
|
||||
- `variant`: `default` (plain row), `separator` (centered label with rules on
|
||||
each side), `border` (bottom-bordered row).
|
||||
- `MarkerIcon` holds a leading icon; `MarkerContent` holds the label.
|
||||
|
||||
**Incorrect:**
|
||||
|
||||
```tsx
|
||||
<div className="flex items-center gap-3 py-2">
|
||||
<Separator className="flex-1" />
|
||||
<span className="text-xs text-muted-foreground">Today</span>
|
||||
<Separator className="flex-1" />
|
||||
</div>
|
||||
```
|
||||
|
||||
**Correct:**
|
||||
|
||||
```tsx
|
||||
<Marker variant="separator">
|
||||
<MarkerContent>Today</MarkerContent>
|
||||
</Marker>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Streaming, anchoring, and jump-to-latest are built in
|
||||
|
||||
`MessageScroller` handles the behavior that chat UIs usually reinvent. Don't
|
||||
write a `useStickToBottom` hook, a `ResizeObserver`, or manual `scrollTop` math.
|
||||
|
||||
- **Follow the live edge while streaming.** `MessageScrollerProvider` with
|
||||
`autoScroll` keeps the view pinned to new content and yields the moment the
|
||||
user scrolls up. Streaming token updates that grow the last message are
|
||||
followed automatically.
|
||||
- **Anchor a turn.** `scrollAnchor` on a `MessageScrollerItem` marks the row to
|
||||
hold in view (typically the user's message that started the turn).
|
||||
- **Jump to latest.** `MessageScrollerButton` appears when the user scrolls away
|
||||
and scrolls back on click. `direction="end"` (default) or `direction="start"`.
|
||||
It is a self-managing control, so don't gate it behind your own scroll-position
|
||||
state.
|
||||
|
||||
For a "thinking…" indicator while the model generates, apply the `shimmer`
|
||||
utility to text. Don't author a custom keyframe animation. See
|
||||
[styling.md](./styling.md).
|
||||
|
||||
---
|
||||
|
||||
## Escape hatch: the scroller hooks
|
||||
|
||||
For behavior the parts don't expose, read state from the hooks rather than
|
||||
re-implementing the scroller: `useMessageScroller`,
|
||||
`useMessageScrollerVisibility`, and `useMessageScrollerScrollable`. They come
|
||||
from the auto-installed `@shadcn/react` dependency, so there's nothing extra to
|
||||
install. Reach for them only when composition can't express what you need.
|
||||
@@ -51,6 +51,12 @@ This applies to all group-based components:
|
||||
| `MenubarItem` | `MenubarGroup` |
|
||||
| `ContextMenuItem` | `ContextMenuGroup` |
|
||||
| `CommandItem` | `CommandGroup` |
|
||||
| `MessageScrollerItem` | `MessageScrollerContent` |
|
||||
| `Message` (consecutive, same sender) | `MessageGroup` |
|
||||
| `Bubble` (stacked) | `BubbleGroup` |
|
||||
| `Attachment` (in a row) | `AttachmentGroup` |
|
||||
|
||||
Chat components nest in a fixed order (`MessageScrollerProvider` → `MessageScroller` → `MessageScrollerViewport` → `MessageScrollerContent` → `MessageScrollerItem`). See [chat.md](./chat.md).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ See [customization.md](../customization.md) for theming, CSS variables, and addi
|
||||
- No manual dark: color overrides
|
||||
- Use cn() for conditional classes
|
||||
- No manual z-index on overlay components
|
||||
- Use shimmer / scroll-fade utilities, not custom animations
|
||||
|
||||
---
|
||||
|
||||
@@ -160,3 +161,25 @@ import { cn } from "@/lib/utils"
|
||||
## No manual z-index on overlay components
|
||||
|
||||
`Dialog`, `Sheet`, `Drawer`, `AlertDialog`, `DropdownMenu`, `Popover`, `Tooltip`, `HoverCard` handle their own stacking. Never add `z-50` or `z-[999]`.
|
||||
|
||||
---
|
||||
|
||||
## Use shimmer / scroll-fade utilities, not custom animations
|
||||
|
||||
For a live "thinking…" or loading-text shimmer, apply the `shimmer` utility. Don't author a custom `@keyframes` or a `bg-clip-text` gradient sweep.
|
||||
|
||||
For scroll-aware edge fading on a scroll container, use `scroll-fade` (and the axis variants `scroll-fade-x` / `scroll-fade-b`). Don't hand-roll mask gradients. The chat components already apply these internally: `Attachment` shimmers its title during upload, and `MessageScrollerViewport` fades its edges.
|
||||
|
||||
**Incorrect:**
|
||||
|
||||
```tsx
|
||||
<span className="animate-pulse bg-gradient-to-r from-muted-foreground/40 via-foreground/70 to-muted-foreground/40 bg-clip-text text-transparent [animation:shimmer_1.6s_infinite]">
|
||||
Thinking…
|
||||
</span>
|
||||
```
|
||||
|
||||
**Correct:**
|
||||
|
||||
```tsx
|
||||
<span className="shimmer">Thinking…</span>
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user