본문으로 건너뛰기
언어

Bubblestandard

대화 한 줄을 담는 말풍선(버블, bubble)입니다. 보낸 쪽·받은 쪽 정렬, 여러 색 변형, 이모지 반응 표시를 갖습니다. shadcn base-nova 의 원본을 그대로 담은 아이템입니다. 업스트림 메타데이터에 문서 링크가 없습니다.

설치

pnpm
pnpm dlx shadcn@4.23.0 add @uikit/bubble

Default

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>  )}