HextaUI

Bubble

Burbujas de mensajes de chat con variantes, esquinas agrupadas, reacciones y espacio para contenido interactivo.

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

Añade el componente, los tokens del tema de HextaUI y los componentes de HextaUI de los que 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

Siete tratamientos, desde una burbuja primaria intensa hasta contenido ghost sin marco para las respuestas del asistente. destructive tiñe la superficie y colorea los iconos que contiene.

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

Alineación

align="end" empuja una burbuja al final de la fila para los mensajes del usuario actual. Sigue la dirección de lectura.

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

Envuelve los mensajes consecutivos de un mismo remitente en <BubbleGroup />. Las esquinas del lado del remitente se estrechan para que la tanda se lea como un solo bloque, y una burbuja aislada permanece totalmente redonda.

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

Define shape en un grupo para dar estilo a todas sus burbujas, o en una sola burbuja. joined es el valor por defecto.

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

Cola

shape="tail" dibuja una cola en la última burbuja de una tanda, del mismo color que la burbuja. Las burbujas outline y ghost la omiten.

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

Reacciones

<BubbleReactions /> se superpone al borde de la burbuja y reserva su propio espacio, así que la fila siguiente no necesita separación extra. Dale a una fila estática role="img" y una etiqueta, o usa botones para reacciones interactivas.

"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 más

Renderiza un <Collapsible /> como contenido de la burbuja para plegar mensajes largos. La burbuja crece suavemente y el texto oculto sigue siendo buscable.

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

Renderiza una burbuja como trigger de un tooltip para mostrar detalles como la hora de lectura. Añade tabIndex para que los usuarios de teclado puedan llegar a ella.

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

Renderiza el contenido como un <button> para que toda la burbuja sea el trigger de un popover, por ejemplo para explicar una entrega fallida.

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

Contenido largo

Las cadenas sin espacios, las URL, los emoji y el texto CJK se ajustan dentro de la burbuja en lugar de desbordar una columna estrecha.

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

Respuesta anidada

Una burbuja citada dentro de otra mantiene los radios concéntricos con su padre.

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

De derecha a izquierda

La alineación, las esquinas unidas, las colas y las reacciones se reflejan en diseños de derecha a izquierda.

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>
  )
}
  • Las burbujas son contenedores simples. Envuelve una conversación en una lista o en una región role="log" si los mensajes nuevos deben anunciarse.
  • Las filas de reacciones solo con emoji necesitan role="img" y un aria-label que detalle las reacciones.
  • El contenido interactivo renderizado como botón o enlace recibe un anillo de foco que sigue las esquinas de la burbuja.

Cada parte renderiza un <div> por defecto y acepta sus atributos, además de una prop render para cambiar el elemento.

PropTipoPredeterminado
variant
"default" | "secondary" | "muted" | "tinted" | "outline" | "ghost" | "destructive""default"
align
"start" | "end""start"
shapeSe hereda de BubbleGroup si no se define.
"uniform" | "joined" | "tail""joined"
render
ReactElement | (props, state) => ReactElement<div>
AtributoDescripción
data-slot="bubble"Selecciona las burbujas en CSS.
data-variantLa variante actual.
data-alignLa alineación actual.
data-shapeLa forma resuelta.
--bubble-bgEl color de la superficie, que también usa la cola.
--bubble-fgEl color del texto.
--bubble-borderEl color del anillo.
--bubble-radiusEl radio de las esquinas.
--bubble-radius-joinedEl radio más estrecho usado entre burbujas agrupadas.
--bubble-accentColor de los iconos dentro del contenido. Lo define destructive.
PropTipoPredeterminado
renderRenderiza un <button> o <a> para hacerlo interactivo.
ReactElement | (props, state) => ReactElement<div>
AtributoDescripción
data-slot="bubble-content"Selecciona la superficie de la burbuja en CSS.
PropTipoPredeterminado
shapeSe aplica a todas las burbujas dentro del grupo.
"uniform" | "joined" | "tail"–
render
ReactElement | (props, state) => ReactElement<div>
AtributoDescripción
data-slot="bubble-group"Apunta a los grupos en CSS.
data-shapeLa forma del grupo, cuando está definida.
PropTipoPredeterminado
side
"top" | "bottom""bottom"
align
"start" | "end""end"
render
ReactElement | (props, state) => ReactElement<div>
AtributoDescripción
data-slot="bubble-reactions"Selecciona las filas de reacciones en CSS.
data-sideEl lado actual.
data-alignLa alineación actual.

Usado en bloques

Bloques que se construyen sobre Bubble.