HextaUI

Bubble

Varyantlı, köşeleri gruplanmış, tepkileri olan ve etkileşimli içeriğe yer açan sohbet mesajı balonları.

I checked the registry output and removed the stale route.
Nice, thanks!
Did the preview deploy pick it up?
Yes. The preview at build 1482 serves the new route, and the old one now returns a 404 as expected.
import {
  Bubble,
  BubbleContent,
  BubbleGroup,
  BubbleReactions,
} from "@/components/ui/bubble"

export function BubbleDemo() {
  return (
    <div className="flex w-full max-w-md flex-col gap-3">
      <Bubble variant="secondary">
        <BubbleContent>
          I checked the registry output and removed the stale route.
        </BubbleContent>
        <BubbleReactions role="img" aria-label="Reactions: thumbs up">
          <span>👍</span>
        </BubbleReactions>
      </Bubble>
      <BubbleGroup>
        <Bubble align="end">
          <BubbleContent>Nice, thanks!</BubbleContent>
        </Bubble>
        <Bubble align="end">
          <BubbleContent>Did the preview deploy pick it up?</BubbleContent>
        </Bubble>
      </BubbleGroup>
      <Bubble variant="ghost">
        <BubbleContent>
          Yes. The preview at build 1482 serves the new route, and the old one
          now returns a 404 as expected.
        </BubbleContent>
      </Bubble>
    </div>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/bubble.json

Bileşeni, HextaUI tema token'larını ve bileşenin bağımlı olduğu tüm HextaUI bileşenlerini ekler.

import {
  Bubble,
  BubbleContent,
  BubbleGroup,
  BubbleReactions,
} from "@/components/ui/bubble"
<BubbleGroup>
  <Bubble variant="secondary">
    <BubbleContent>Did the deploy land?</BubbleContent>
  </Bubble>
  <Bubble align="end">
    <BubbleContent>It did.</BubbleContent>
  </Bubble>
</BubbleGroup>
BubbleGroup
└── Bubble
    ├── BubbleContent
    └── BubbleReactions

Varyantlar

Güçlü bir primary balondan asistan yanıtları için çerçevesiz ghost içeriğe kadar yedi görünüm. destructive yüzeyi tonlar ve içindeki simgeleri renklendirir.

import { IconAlertCircle } from "@tabler/icons-react"

import { Bubble, BubbleContent } from "@/components/ui/bubble"

export function BubbleVariants() {
  return (
    <div className="flex w-full max-w-md flex-col gap-3">
      <Bubble>
        <BubbleContent>Pushed the fix, can you take a look?</BubbleContent>
      </Bubble>
      <Bubble variant="secondary">
        <BubbleContent>Looks good. Merging after CI passes.</BubbleContent>
      </Bubble>
      <Bubble variant="muted">
        <BubbleContent>Jamie joined the conversation</BubbleContent>
      </Bubble>
      <Bubble variant="tinted">
        <BubbleContent>Pinned: release notes are in the doc.</BubbleContent>
      </Bubble>
      <Bubble variant="outline">
        <BubbleContent>Here is the summary you asked for.</BubbleContent>
      </Bubble>
      <Bubble variant="ghost">
        <BubbleContent>
          Ghost bubbles drop the frame so assistant replies can span the full
          row and read like a document.
        </BubbleContent>
      </Bubble>
      <Bubble variant="destructive">
        <BubbleContent>
          <IconAlertCircle /> Message failed to send. Tap to retry.
        </BubbleContent>
      </Bubble>
    </div>
  )
}

Hizalama

align="end" bir balonu, geçerli kullanıcının mesajları için satırın sonuna iter. Okuma yönünü izler.

import { Bubble, BubbleContent } from "@/components/ui/bubble"

export function BubbleAlignment() {
  return (
    <div className="flex w-full max-w-md flex-col gap-3">
      <Bubble variant="secondary" align="start">
        <BubbleContent>Aligned to the start.</BubbleContent>
      </Bubble>
      <Bubble align="end">
        <BubbleContent>Aligned to the end.</BubbleContent>
      </Bubble>
    </div>
  )
}

Group

Aynı göndericinin ardışık mesajlarını <BubbleGroup /> içine sarın. Gönderen tarafındaki köşeler sıkılaşır; böylece dizi tek blok gibi okunur, tek başına bir balon ise tamamen yuvarlak kalır.

import { Bubble, BubbleContent, BubbleGroup } from "@/components/ui/bubble"

export function BubbleGroupDemo() {
  return (
    <div className="flex w-full max-w-md flex-col gap-3">
      <BubbleGroup>
        <Bubble variant="secondary">
          <BubbleContent>Hey!</BubbleContent>
        </Bubble>
        <Bubble variant="secondary">
          <BubbleContent>Did the deploy land?</BubbleContent>
        </Bubble>
        <Bubble variant="secondary">
          <BubbleContent>
            Asking because the dashboard still shows the old build number and I
            want to confirm before I post the announcement.
          </BubbleContent>
        </Bubble>
      </BubbleGroup>
      <BubbleGroup>
        <Bubble align="end">
          <BubbleContent>It did.</BubbleContent>
        </Bubble>
        <Bubble align="end">
          <BubbleContent>Hard refresh 🙏</BubbleContent>
        </Bubble>
      </BubbleGroup>
      <BubbleGroup>
        <Bubble variant="outline">
          <BubbleContent>A single bubble in a group stays round.</BubbleContent>
        </Bubble>
      </BubbleGroup>
    </div>
  )
}

Şekiller

Bir grubun tüm balonlarını stillemek için grup üzerinde ya da tek bir balonda shape ayarlayın. Varsayılan joined'dır.

import { Bubble, BubbleContent, BubbleGroup } from "@/components/ui/bubble"

const shapes = [
  { shape: "uniform", description: "Every bubble fully round." },
  {
    shape: "joined",
    description: "Grouped corners tighten on the sender side.",
  },
  { shape: "tail", description: "The last bubble of a group gets a tail." },
] as const

export function BubbleShapes() {
  return (
    <div className="flex w-full max-w-md flex-col gap-10">
      {shapes.map(({ shape, description }) => (
        <div key={shape} className="flex flex-col gap-3">
          <p className="text-xs text-muted-foreground">
            <span className="font-medium text-foreground">{shape}</span>{" "}
            {description}
          </p>
          <BubbleGroup shape={shape}>
            <Bubble variant="secondary">
              <BubbleContent>Hey!</BubbleContent>
            </Bubble>
            <Bubble variant="secondary">
              <BubbleContent>Did the deploy land?</BubbleContent>
            </Bubble>
          </BubbleGroup>
          <BubbleGroup shape={shape}>
            <Bubble align="end">
              <BubbleContent>It did.</BubbleContent>
            </Bubble>
            <Bubble align="end">
              <BubbleContent>
                Hard refresh and you should see build 1482 on the dashboard.
              </BubbleContent>
            </Bubble>
          </BubbleGroup>
          <Bubble shape={shape} variant="secondary">
            <BubbleContent>Perfect 🙏</BubbleContent>
          </Bubble>
        </div>
      ))}
    </div>
  )
}

Kuyruk

shape="tail" bir dizinin son balonuna balon rengiyle eşleşen bir kuyruk çizer. Outline ve ghost balonlar bunu atlar.

import {
  Bubble,
  BubbleContent,
  BubbleGroup,
  BubbleReactions,
} from "@/components/ui/bubble"

export function BubbleTail() {
  return (
    <div className="flex w-full max-w-md flex-col gap-3">
      <Bubble shape="tail">
        <BubbleContent>Pushed the fix, can you take a look?</BubbleContent>
      </Bubble>
      <Bubble shape="tail" variant="secondary" align="end">
        <BubbleContent>Looks good. Merging after CI passes.</BubbleContent>
      </Bubble>
      <Bubble shape="tail" variant="muted">
        <BubbleContent>Jamie joined the conversation</BubbleContent>
      </Bubble>
      <Bubble shape="tail" variant="tinted" align="end">
        <BubbleContent>Pinned: release notes are in the doc.</BubbleContent>
      </Bubble>
      <Bubble shape="tail" variant="outline">
        <BubbleContent>Outline bubbles never get a tail.</BubbleContent>
      </Bubble>
      <Bubble shape="tail" variant="destructive" align="end">
        <BubbleContent>Message failed to send. Tap to retry.</BubbleContent>
      </Bubble>
      <Bubble shape="tail" align="end">
        <BubbleContent>Reactions sit on the top corner.</BubbleContent>
        <BubbleReactions side="top" align="start" role="img" aria-label="Heart">
          <span>❤️</span>
        </BubbleReactions>
      </Bubble>
      <Bubble shape="tail" variant="secondary">
        <BubbleContent>ok</BubbleContent>
      </Bubble>
      <BubbleGroup shape="tail">
        <Bubble variant="secondary">
          <BubbleContent>Only the last bubble in a group</BubbleContent>
        </Bubble>
        <Bubble variant="secondary">
          <BubbleContent>gets the tail.</BubbleContent>
        </Bubble>
      </BubbleGroup>
    </div>
  )
}

Tepkiler

<BubbleReactions /> balon kenarıyla çakışır ve kendi alanını ayırır; bu yüzden sonraki satır için ek boşluk gerekmez. Statik bir satıra role="img" ve bir etiket verin veya etkileşimli tepkiler için düğmeler kullanın.

"use client"

import * as React from "react"
import {
  IconFlame,
  IconHeart,
  IconMoodSmile,
  IconThumbUp,
} from "@tabler/icons-react"

import { Bubble, BubbleContent, BubbleReactions } from "@/components/ui/bubble"
import { Button } from "@/components/ui/button"

function ReactionToggle({
  label,
  icon,
  initial = false,
}: {
  label: string
  icon: React.ReactNode
  initial?: boolean
}) {
  const [pressed, setPressed] = React.useState(initial)

  return (
    <Button
      aria-label={label}
      aria-pressed={pressed}
      variant={pressed ? "default" : "secondary"}
      size="icon-xs"
      onClick={() => setPressed(!pressed)}
    >
      {icon}
    </Button>
  )
}

export function BubbleReactionsDemo() {
  return (
    <div className="flex w-full max-w-md flex-col gap-6">
      <Bubble variant="secondary">
        <BubbleContent>Shipped the new onboarding flow.</BubbleContent>
        <BubbleReactions
          role="img"
          aria-label="Reactions: thumbs up, fire, and 8 more"
        >
          <span>👍</span>
          <span>🔥</span>
          <span>+8</span>
        </BubbleReactions>
      </Bubble>
      <Bubble align="end">
        <BubbleContent>Reactions can anchor to the top.</BubbleContent>
        <BubbleReactions side="top" align="start" role="img" aria-label="Heart">
          <span>❤️</span>
        </BubbleReactions>
      </Bubble>
      <Bubble variant="secondary">
        <BubbleContent>ok</BubbleContent>
        <BubbleReactions
          role="img"
          aria-label="Reactions: party, eyes, rocket, check"
        >
          <span>🎉</span>
          <span>👀</span>
          <span>🚀</span>
          <span>✅</span>
        </BubbleReactions>
      </Bubble>
      <Bubble variant="outline">
        <BubbleContent>Interactive reactions use buttons.</BubbleContent>
        <BubbleReactions align="start">
          <ReactionToggle label="Thumbs up" icon={<IconThumbUp />} initial />
          <ReactionToggle label="Heart" icon={<IconHeart />} />
          <ReactionToggle label="Fire" icon={<IconFlame />} />
          <ReactionToggle label="Add reaction" icon={<IconMoodSmile />} />
        </BubbleReactions>
      </Bubble>
    </div>
  )
}

Daha fazla göster

Uzun mesajları katlamak için balon içeriği olarak bir <Collapsible /> oluşturun. Balon düzgünce büyür ve gizli metin aranabilir kalır.

import { IconChevronDown } from "@tabler/icons-react"

import { Bubble, BubbleContent } from "@/components/ui/bubble"
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
} from "@/components/ui/collapsible"

export function BubbleShowMore() {
  return (
    <div className="flex w-full max-w-md flex-col gap-3">
      <Bubble variant="secondary">
        <Collapsible render={<BubbleContent />}>
          <p>
            Here is the full incident timeline. The first alert fired at 09:12
            when p95 latency crossed 800ms on the checkout service.
          </p>
          <CollapsibleContent>
            <p className="pt-2">
              At 09:20 we rolled back the connection pool change. Latency
              recovered by 09:24 and error rates returned to baseline by 09:31.
              A follow-up will add a canary stage for pool configuration.
            </p>
          </CollapsibleContent>
          <CollapsibleTrigger
            render={
              <button
                type="button"
                className="mt-1 inline-flex cursor-pointer items-center gap-1 rounded-sm font-medium underline-offset-4 outline-none hover:underline focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:outline-hidden"
              />
            }
          >
            <span className="group-data-panel-open/collapsible-trigger:hidden">
              Show more
            </span>
            <span className="hidden group-data-panel-open/collapsible-trigger:inline">
              Show less
            </span>
            <IconChevronDown className="size-4 transition-transform duration-200 ease-out-cubic group-data-panel-open/collapsible-trigger:rotate-180 motion-reduce:transition-none" />
          </CollapsibleTrigger>
        </Collapsible>
      </Bubble>
    </div>
  )
}

Tooltip

Okunma zamanı gibi ayrıntıları göstermek için bir balonu tooltip trigger'ı olarak oluşturun. Klavye kullanıcılarının ulaşabilmesi için tabIndex ekleyin.

import { Bubble, BubbleContent } from "@/components/ui/bubble"
import {
  Tooltip,
  TooltipContent,
  TooltipTrigger,
} from "@/components/ui/tooltip"

export function BubbleTooltip() {
  return (
    <div className="flex w-full max-w-md flex-col gap-3">
      <Tooltip>
        <TooltipTrigger render={<Bubble align="end" tabIndex={0} />}>
          <BubbleContent>Hover to see when this was read.</BubbleContent>
        </TooltipTrigger>
        <TooltipContent side="left" sideOffset={8}>
          Read 9:41 AM
        </TooltipContent>
      </Tooltip>
    </div>
  )
}

Popover

Tüm balonu bir popover trigger'ı yapmak için içeriği bir <button> olarak oluşturun; örneğin başarısız bir teslimatı açıklamak için.

import { IconAlertCircle } from "@tabler/icons-react"

import { Bubble, BubbleContent } from "@/components/ui/bubble"
import {
  Popover,
  PopoverContent,
  PopoverDescription,
  PopoverHeader,
  PopoverTitle,
  PopoverTrigger,
} from "@/components/ui/popover"

export function BubblePopover() {
  return (
    <div className="flex w-full max-w-md flex-col gap-3">
      <Popover>
        <Bubble variant="destructive" align="end">
          <PopoverTrigger
            render={<BubbleContent render={<button type="button" />} />}
          >
            <IconAlertCircle /> Not delivered. Tap for details.
          </PopoverTrigger>
        </Bubble>
        <PopoverContent align="end" className="w-64">
          <PopoverHeader>
            <PopoverTitle>Delivery failed</PopoverTitle>
            <PopoverDescription>
              The recipient&apos;s server rejected the message (550: mailbox
              unavailable).
            </PopoverDescription>
          </PopoverHeader>
        </PopoverContent>
      </Popover>
    </div>
  )
}

Uzun içerik

Kırılmayan dizeler, URL'ler, emojiler ve CJK metni, dar bir sütunu aşmak yerine balonun içinde sarar.

import { Bubble, BubbleContent } from "@/components/ui/bubble"

export function BubbleLongContent() {
  return (
    <div className="flex w-72 max-w-full flex-col gap-3">
      <Bubble variant="secondary">
        <BubbleContent>
          Supercalifragilisticexpialidocious_unbroken_string_that_never_ends_1234567890
        </BubbleContent>
      </Bubble>
      <Bubble align="end">
        <BubbleContent>
          https://example.com/a/very/long/path/that/keeps/going/and/going?with=query&amp;params=true
        </BubbleContent>
      </Bubble>
      <Bubble variant="outline">
        <BubbleContent>
          👩‍👩‍👧‍👦👨🏽‍💻🏳️‍🌈 你好世界,这是一个很长的中文句子用于测试换行
        </BubbleContent>
      </Bubble>
      <Bubble variant="muted" align="end">
        <BubbleContent>k</BubbleContent>
      </Bubble>
      <Bubble variant="tinted">
        <BubbleContent> </BubbleContent>
      </Bubble>
    </div>
  )
}

İç içe yanıt

Başka bir balonun içindeki alıntılanmış bir balon, yarıçaplarını üst öğesiyle eş merkezli tutar.

import { Bubble, BubbleContent } from "@/components/ui/bubble"

export function BubbleNested() {
  return (
    <div className="flex w-full max-w-md flex-col gap-3">
      <Bubble align="end">
        <BubbleContent>
          <Bubble variant="tinted">
            <BubbleContent>Original: can we ship Friday?</BubbleContent>
          </Bubble>
          <p className="pt-2">Yes, Friday works.</p>
        </BubbleContent>
      </Bubble>
    </div>
  )
}

Sağdan sola

Hizalama, birleşik köşeler, kuyruklar ve tepkilerin tümü sağdan sola düzenlerde yansıtılır.

import {
  Bubble,
  BubbleContent,
  BubbleGroup,
  BubbleReactions,
} from "@/components/ui/bubble"

export function BubbleRtl() {
  return (
    <div dir="rtl" className="flex w-full max-w-md flex-col gap-3">
      <BubbleGroup>
        <Bubble variant="secondary">
          <BubbleContent>مرحبا! هل وصل التحديث؟</BubbleContent>
        </Bubble>
        <Bubble variant="secondary">
          <BubbleContent>أنا أنتظر التأكيد.</BubbleContent>
          <BubbleReactions role="img" aria-label="إعجاب">
            <span>👍</span>
            <span dir="ltr">+3</span>
          </BubbleReactions>
        </Bubble>
      </BubbleGroup>
      <BubbleGroup shape="tail">
        <Bubble align="end">
          <BubbleContent>نعم، تم النشر.</BubbleContent>
        </Bubble>
        <Bubble align="end">
          <BubbleContent render={<button type="button" />}>
            أرني السجل
          </BubbleContent>
        </Bubble>
      </BubbleGroup>
    </div>
  )
}
  • Balonlar düz kapsayıcılardır. Yeni mesajların duyurulmasını istiyorsanız bir konuşmayı bir listeye veya role="log" bölgesine sarın.
  • Yalnızca emojiden oluşan tepki satırları role="img" ve tepkileri tek tek belirten bir aria-label gerektirir.
  • Düğme veya bağlantı olarak oluşturulan etkileşimli içerik, balonun köşelerini izleyen bir odak halkası alır.

Her parça varsayılan olarak bir <div> oluşturur ve onun niteliklerini, ayrıca öğeyi değiştirmek için bir render prop'unu kabul eder.

PropTürVarsayılan
variant
"default" | "secondary" | "muted" | "tinted" | "outline" | "ghost" | "destructive""default"
align
"start" | "end""start"
shapeAyarlanmazsa BubbleGroup'tan devralınır.
"uniform" | "joined" | "tail""joined"
render
ReactElement | (props, state) => ReactElement<div>
ÖznitelikAçıklama
data-slot="bubble"CSS'te balonları hedefleyin.
data-variantGeçerli varyant.
data-alignGeçerli hizalama.
data-shapeÇözümlenen şekil.
--bubble-bgKuyruğun da kullandığı yüzey rengi.
--bubble-fgMetin rengi.
--bubble-borderHalka rengi.
--bubble-radiusKöşe yarıçapı.
--bubble-radius-joinedGruplanmış balonlar arasında kullanılan daha dar yarıçap.
--bubble-accentİçerik içindeki simge rengi. destructive tarafından ayarlanır.
PropTürVarsayılan
renderEtkileşimli yapmak için bir <button> veya <a> oluşturun.
ReactElement | (props, state) => ReactElement<div>
ÖznitelikAçıklama
data-slot="bubble-content"CSS'te balon yüzeyini hedefleyin.
PropTürVarsayılan
shapeGrubun içindeki her balona uygulanır.
"uniform" | "joined" | "tail"–
render
ReactElement | (props, state) => ReactElement<div>
ÖznitelikAçıklama
data-slot="bubble-group"CSS'te grupları hedefleyin.
data-shapeAyarlanmışsa grubun şekli.
PropTürVarsayılan
side
"top" | "bottom""bottom"
align
"start" | "end""end"
render
ReactElement | (props, state) => ReactElement<div>
ÖznitelikAçıklama
data-slot="bubble-reactions"CSS'te tepki satırlarını hedefleyin.
data-sideGeçerli taraf.
data-alignGeçerli hizalama.

Kullanıldığı bloklar

Bubble üzerine kurulan bloklar.