Bubblestandard
대화 한 줄을 담는 말풍선(버블, bubble)입니다. 보낸 쪽·받은 쪽 정렬, 여러 색 변형, 이모지 반응 표시를 갖습니다. shadcn base-nova 의 원본을 그대로 담은 아이템입니다. 업스트림 메타데이터에 문서 링크가 없습니다.
설치
pnpm
pnpm dlx shadcn@4.23.0 add @uikit/bubbleDefault
bubble-example.tsx
"use client"
import * as React from "react"import { toast } from "sonner"
import { Example, ExampleWrapper,} from "@/components/example"import { Bubble, BubbleContent, BubbleGroup, BubbleReactions,} from "@/components/ui/bubble"import { Button } from "@/components/ui/button"import { Collapsible, CollapsibleTrigger,} from "@/components/ui/collapsible"import { Marker, MarkerContent } from "@/components/ui/marker"import { ChevronDownIcon, ThumbsDownIcon, ThumbsUpIcon } from "lucide-react"
export default function BubbleExample() { return ( <ExampleWrapper> <BubbleSizes /> <BubbleVariants /> <BubbleAlignment /> <BubbleGrouped /> <BubbleCollapsible /> <BubbleButtonLinks /> <BubbleWithReactions /> <BubbleReactionsButtons /> </ExampleWrapper> )}
function BubbleVariants() { return ( <Example title="Variants"> <div className="flex w-full max-w-md flex-col gap-8"> <Bubble> <BubbleContent> Default bubbles use the primary color for the active user side of a chat. </BubbleContent> </Bubble> <Bubble variant="secondary"> <BubbleContent> Secondary bubbles are the standard neutral surface for assistant and conversation content. </BubbleContent> </Bubble> <Bubble variant="muted"> <BubbleContent> Muted bubbles lower the emphasis for quiet system notes or for displaying supporting content. </BubbleContent> </Bubble> <Bubble variant="tinted" align="end"> <BubbleContent> Tinted bubbles use a softer primary tint when primary fill is too strong. </BubbleContent> </Bubble> <Bubble variant="outline"> <BubbleContent> Outline bubbles can be used to frame message content and give it a border. </BubbleContent> </Bubble> <Bubble variant="destructive"> <BubbleContent> Destructive bubbles flag errors or failed actions in a conversation. </BubbleContent> </Bubble> <Bubble variant="ghost"> <BubbleContent> <span className="whitespace-pre-wrap"> {`Ghost bubbles work for assistant text and other content that should not be framed.
This is perfect for assistant messages that should not have a frame and can take the full width of the container.
Ghost bubbles are full width and can take the full width of the container.`} </span> </BubbleContent> </Bubble> </div> </Example> )}
function BubbleSizes() { return ( <Example title="Sizes"> <div className="flex w-full max-w-md flex-col gap-8"> <Bubble> <BubbleContent>This is a one line bubble.</BubbleContent> </Bubble> <Bubble> <BubbleContent> This bubble has multiple lines. It should wrap to the next line and you should see a different radius on the corners. </BubbleContent> </Bubble> <Bubble> <BubbleContent> <p>This bubble has multiple lines.</p> <p> It should wrap to the next line and you should see a different radius on the corners. </p> <p>Here is some more text to see how it wraps.</p> </BubbleContent> </Bubble> </div> </Example> )}
function BubbleGrouped() { return ( <Example title="Grouped"> <div className="flex w-full max-w-md flex-col gap-8"> <BubbleGroup> <Bubble variant="secondary"> <BubbleContent>I finished the audit pass.</BubbleContent> </Bubble> <Bubble variant="secondary"> <BubbleContent> The registry output looks clean, but I found one stale route. </BubbleContent> </Bubble> <Bubble variant="secondary"> <BubbleContent>Want me to remove it now?</BubbleContent> </Bubble> </BubbleGroup> <BubbleGroup> <Bubble variant="tinted" align="end"> <BubbleContent>Yes, clean that up.</BubbleContent> </Bubble> <Bubble variant="tinted" align="end"> <BubbleContent>Then rerun the registry build.</BubbleContent> </Bubble> </BubbleGroup> </div> </Example> )}
const text = `The accessibility review found two focus states that were visually too subtle in dark mode.
I checked the dialog, menu, and drawer paths because each one renders focusable controls inside a layered surface.
The dialog and drawer are fine. The menu needs the hover and focus tokens split so keyboard focus stays visible when the pointer is not involved.
I also recommend keeping the change in the style file instead of the primitive so the other themes can choose their own focus treatment later.`
const previewLength = 180
function BubbleCollapsible() { const [open, setOpen] = React.useState(false) const isLong = text.length > previewLength const preview = `${text.slice(0, previewLength)}...`
return ( <Example title="Collapsible"> <div className="flex w-full max-w-md flex-col gap-8"> <Collapsible open={open} onOpenChange={setOpen}> <Bubble variant="muted" align="end"> <BubbleContent className="whitespace-pre-line"> <div>{open || !isLong ? text : preview}</div> {isLong ? ( <CollapsibleTrigger render={ <Button variant="link" className="gap-1 p-0 text-muted-foreground" /> } > {open ? "Show less" : "Show more"} <ChevronDownIcon data-icon="inline-end" className="group-data-panel-open/button:rotate-180" /> </CollapsibleTrigger> ) : null} </BubbleContent> </Bubble> </Collapsible> <Bubble variant="ghost"> <BubbleContent> <span className="whitespace-pre-wrap"> {`Ghost bubbles work for assistant text and other content that should not be framed.
This is perfect for assistant messages that should not have a frame and can take the full width of the container.
Use this for content that needs the whole row.`} </span> </BubbleContent> </Bubble> </div> </Example> )}
function BubbleWithReactions() { return ( <Example title="Reaction Placement"> <div className="flex w-full max-w-md flex-col gap-12"> <Marker variant="separator"> <MarkerContent>side=bottom align=end</MarkerContent> </Marker> <Bubble> <BubbleContent>This is a one line message.</BubbleContent> <BubbleReactions side="bottom" align="end" role="img" aria-label="Reaction: thumbs up" > <span>👍</span> </BubbleReactions> </Bubble> <Bubble variant="secondary" align="end"> <BubbleContent> A longer message that wraps across lines so the reaction offset is easier to inspect. </BubbleContent> <BubbleReactions side="bottom" align="start" role="img" aria-label="Reactions: thumbs up, surprised" > <span>👍</span> <span>😮</span> </BubbleReactions> </Bubble> <Bubble variant="tinted"> <BubbleContent> A longer message that wraps across lines so the reaction offset is easier to inspect. </BubbleContent> <BubbleReactions side="bottom" align="end" role="img" aria-label="Reactions: thumbs up, surprised, fire, eyes, and 8 more" > <span>👍</span> <span>😮</span> <span>🔥</span> <span>👀</span> <span>+8</span> </BubbleReactions> </Bubble> <Marker variant="separator"> <MarkerContent>side=bottom align=start</MarkerContent> </Marker> <Bubble variant="secondary"> <BubbleContent>This is a one line message.</BubbleContent> <BubbleReactions side="bottom" align="start" role="img" aria-label="Reaction: fire" > <span>🔥</span> </BubbleReactions> </Bubble> <Bubble variant="secondary"> <BubbleContent> A longer message that wraps across lines so the reaction offset is easier to inspect. </BubbleContent> <BubbleReactions side="bottom" align="start" role="img" aria-label="Reactions: thumbs up, surprised, fire, eyes" > <span>👍</span> <span>😮</span> <span>🔥</span> <span>👀</span> </BubbleReactions> </Bubble> <Marker variant="separator"> <MarkerContent>side=top align=start</MarkerContent> </Marker> <Bubble variant="secondary"> <BubbleContent>This is a one line message.</BubbleContent> <BubbleReactions side="top" align="start" role="img" aria-label="Reaction: fire" > <span>🔥</span> </BubbleReactions> </Bubble> <Bubble variant="secondary"> <BubbleContent> A longer message that wraps across lines so the reaction offset is easier to inspect. </BubbleContent> <BubbleReactions side="top" align="start" role="img" aria-label="Reactions: thumbs up, surprised, fire, eyes" > <span>👍</span> <span>😮</span> <span>🔥</span> <span>👀</span> </BubbleReactions> </Bubble> <Marker variant="separator"> <MarkerContent>side=bottom align=end</MarkerContent> </Marker> <Bubble variant="muted"> <BubbleContent>This is a one line message.</BubbleContent> <BubbleReactions side="top" align="end" role="img" aria-label="Reaction: thumbs up" > <span>👍</span> </BubbleReactions> </Bubble> <Bubble variant="muted"> <BubbleContent> A longer message that wraps across lines so the reaction offset. </BubbleContent> <BubbleReactions side="top" align="end" role="img" aria-label="Reactions: thumbs up, surprised, fire, eyes" className="px-1.5 py-0.5" > <span>👍</span> <span>😮</span> <span>🔥</span> <span>👀</span> </BubbleReactions> </Bubble> </div> </Example> )}
function BubbleReactionsButtons() { return ( <Example title="Reactions Buttons"> <div className="flex w-full max-w-md flex-col gap-8"> <Bubble> <BubbleContent>This is a one line message.</BubbleContent> <BubbleReactions> <Button variant="outline" size="xs" onClick={() => toast("You clicked the button in the bubble reaction") } > Button </Button> </BubbleReactions> </Bubble> <Bubble align="end"> <BubbleContent>This is a one line message.</BubbleContent> <BubbleReactions align="start"> <Button variant="ghost" size="icon-xs" onClick={() => toast("Confetti!")} > 🎉 </Button> </BubbleReactions> </Bubble> <Bubble variant="tinted"> <BubbleContent> We are going to the movies first then dinner. Are you in? </BubbleContent> <BubbleReactions className="gap-1 bg-background"> <Button variant="secondary" size="icon-xs" aria-label="Thumbs up" onClick={() => toast("You agree!")} > <ThumbsUpIcon /> </Button> <Button variant="secondary" size="icon-xs" aria-label="Thumbs down" onClick={() => toast("You disagree!")} > <ThumbsDownIcon /> </Button> </BubbleReactions> </Bubble> </div> </Example> )}
function BubbleAlignment() { return ( <Example title="Alignment"> <div className="flex w-full max-w-md flex-col gap-8"> <Bubble variant="muted"> <BubbleContent>This bubble is aligned to the start.</BubbleContent> </Bubble> <Bubble align="end"> <BubbleContent>This bubble is aligned to the end.</BubbleContent> </Bubble> <Bubble variant="muted"> <BubbleContent> This multiline bubble is aligned to the start. The corners should adjust when the text wraps to show the grouped side of the conversation. </BubbleContent> </Bubble> <Bubble align="end"> <BubbleContent> This multiline bubble is aligned to the end. It should sit on the opposite side with the matching corner radius for wrapped text. </BubbleContent> </Bubble> </div> </Example> )}
const quickReplies = [ { label: "I need help with my account.", message: "I need help with my account.", }, { label: "I forgot my password.", message: "I forgot my password.", }, { label: "I have another question. I'd like to talk to a human. Can you help me?", message: "I have another question.", },]
function BubbleButtonLinks() { return ( <Example title="Button & Links"> <div className="flex w-full max-w-md flex-col gap-8"> <Bubble> <BubbleContent render={<a href="#" />}> This bubble is a link. </BubbleContent> </Bubble> <Bubble variant="secondary"> <BubbleContent render={<button type="button" />}> This one is a button you can click. </BubbleContent> </Bubble> <Bubble variant="muted"> <BubbleContent render={<button type="button" />}> You can also do tinted buttons. Even ones that are multilines. </BubbleContent> </Bubble> <Marker variant="separator"> <MarkerContent>Chat Suggestions</MarkerContent> </Marker> <Bubble> <BubbleContent>How can I help you today?</BubbleContent> </Bubble> <BubbleGroup> {quickReplies.map((reply) => ( <Bubble key={reply.label} variant="outline" align="end"> <BubbleContent className="border-dashed border-primary" render={ <button type="button" onClick={() => toast(reply.message)} /> } > {reply.label} </BubbleContent> </Bubble> ))} </BubbleGroup> </div> </Example> )}