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:
shadcn
2026-06-29 20:18:46 +04:00
committed by GitHub
parent af79276f7e
commit 02e398ab73
5 changed files with 292 additions and 1 deletions

View File

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

View File

@@ -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
View 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.

View File

@@ -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).
---

View File

@@ -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>
```