HextaUI

Bubble

Balões de mensagem de chat com variantes, cantos agrupados, reações e espaço para conteúdo interativo.

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

Adiciona o componente, os tokens de tema do HextaUI e quaisquer componentes do HextaUI dos quais ele depende.

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

Variantes

Sete tratamentos, de um balão primary marcante a conteúdo ghost sem moldura para respostas do assistente. destructive tinge a superfície e colore os ícones dentro dela.

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

Alinhamento

align="end" empurra um balão para o fim da linha, para as mensagens do usuário atual. Segue a direção de leitura.

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

Grupo

Envolva mensagens consecutivas de um mesmo remetente em <BubbleGroup />. Os cantos do lado do remetente se fecham para que a sequência seja lida como um bloco, e um balão isolado continua totalmente arredondado.

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

Formas

Defina shape em um grupo para estilizar todos os seus balões, ou em um único balão. joined é o padrão.

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

Cauda

shape="tail" desenha uma cauda no último balão de uma sequência, na cor do balão. Balões outline e ghost não a têm.

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

Reações

<BubbleReactions /> sobrepõe a borda do balão e reserva o próprio espaço, então a próxima linha não precisa de espaçamento extra. Dê a uma linha estática role="img" e um rótulo, ou use botões para reações interativas.

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

Mostrar mais

Renderize um <Collapsible /> como conteúdo do balão para recolher mensagens longas. O balão cresce suavemente e o texto oculto continua pesquisável.

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

Renderize um balão como gatilho de tooltip para mostrar detalhes como o horário de leitura. Adicione tabIndex para que usuários de teclado consigam alcançá-lo.

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

Renderize o conteúdo como um <button> para transformar o balão inteiro em gatilho de popover, por exemplo para explicar uma falha de entrega.

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

Conteúdo longo

Strings sem quebra, URLs, emoji e texto CJK são quebrados dentro do balão em vez de transbordar uma coluna estreita.

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

Resposta aninhada

Um balão citado dentro de outro mantém os raios concêntricos com o pai.

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

Da direita para a esquerda

O alinhamento, os cantos unidos, as caudas e as reações são espelhados em layouts da direita para a esquerda.

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>
  )
}
  • Os balões são contêineres simples. Envolva uma conversa em uma lista ou em uma região role="log" se novas mensagens devem ser anunciadas.
  • Linhas de reações só com emoji precisam de role="img" e de um aria-label que descreva as reações.
  • Conteúdo interativo renderizado como botão ou link ganha um anel de foco que segue os cantos do balão.

Cada parte renderiza um <div> por padrão e aceita seus atributos, além de uma prop render para trocar o elemento.

PropTipoPadrão
variant
"default" | "secondary" | "muted" | "tinted" | "outline" | "ghost" | "destructive""default"
align
"start" | "end""start"
shapeHerdado de BubbleGroup quando não definido.
"uniform" | "joined" | "tail""joined"
render
ReactElement | (props, state) => ReactElement<div>
AtributoDescrição
data-slot="bubble"Seleciona os balões no CSS.
data-variantA variante atual.
data-alignO alinhamento atual.
data-shapeA forma resolvida.
--bubble-bgA cor da superfície, também usada pela cauda.
--bubble-fgA cor do texto.
--bubble-borderA cor do anel.
--bubble-radiusO raio dos cantos.
--bubble-radius-joinedO raio mais fechado usado entre balões agrupados.
--bubble-accentCor dos ícones dentro do conteúdo. Definida por destructive.
PropTipoPadrão
renderRenderize um <button> ou <a> para torná-lo interativo.
ReactElement | (props, state) => ReactElement<div>
AtributoDescrição
data-slot="bubble-content"Seleciona a superfície do balão no CSS.
PropTipoPadrão
shapeAplica-se a todo balão dentro do grupo.
"uniform" | "joined" | "tail"–
render
ReactElement | (props, state) => ReactElement<div>
AtributoDescrição
data-slot="bubble-group"Selecione grupos no CSS.
data-shapeA forma do grupo, quando definida.
PropTipoPadrão
side
"top" | "bottom""bottom"
align
"start" | "end""end"
render
ReactElement | (props, state) => ReactElement<div>
AtributoDescrição
data-slot="bubble-reactions"Seleciona as linhas de reações no CSS.
data-sideO lado atual.
data-alignO alinhamento atual.

Usado em blocos

Blocos que se baseiam em Bubble.