Sabil UI

Search documentation

Search for a command to run...

Components

Bubble

A chat bubble for conversational interfaces.

Hey there! what's up?
Hey! Want to see chat bubbles?
I can group messages, switch sides, and keep the whole thread easy to scan.
Sure. Hit me with your best demo.
Yes. You are reading a demo that is demoing itself. Very meta. Very on-brand.

Installation#

  1. Install the dependencies

    npm install class-variance-authority cn radix-ui
  2. Copy the source into your project

    components/ui/bubble.tsx
    import * as React from "react"import { cva, type VariantProps } from "class-variance-authority"import { cn } from "cn"import { Slot } from "radix-ui"function BubbleGroup({ className, ...props }: React.ComponentProps<"div">) {  return (    <div      data-slot="bubble-group"      className={cn("flex min-w-0 flex-col gap-2", className)}      {...props}    />  )}const bubbleVariants = cva(  "group/bubble relative flex w-fit max-w-[80%] min-w-0 flex-col gap-1 group-data-[align=end]/message:self-end data-[align=end]:self-end data-[variant=ghost]:max-w-full",  {    variants: {      variant: {        default:          "*:data-[slot=bubble-content]:bg-primary *:data-[slot=bubble-content]:text-primary-foreground [&>[data-slot=bubble-content]:is(button,a):hover]:bg-primary/80",        secondary:          "*:data-[slot=bubble-content]:bg-bubble-strong *:data-[slot=bubble-content]:text-foreground [&>[data-slot=bubble-content]:is(button,a):hover]:bg-[color-mix(in_oklab,var(--bubble-strong),var(--foreground)_6%)]",        muted:          "*:data-[slot=bubble-content]:bg-bubble *:data-[slot=bubble-content]:text-foreground [&>[data-slot=bubble-content]:is(button,a):hover]:bg-bubble-strong",        tinted:          "*:data-[slot=bubble-content]:bg-[oklch(from_var(--primary)_0.93_calc(c*0.4)_h)] *:data-[slot=bubble-content]:text-foreground dark:*:data-[slot=bubble-content]:bg-[oklch(from_var(--primary)_0.3_calc(c*0.4)_h)] [&>[data-slot=bubble-content]:is(button,a):hover]:bg-[oklch(from_var(--primary)_0.88_calc(c*0.5)_h)] dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-[oklch(from_var(--primary)_0.35_calc(c*0.5)_h)]",        outline:          "*:data-[slot=bubble-content]:border-border *:data-[slot=bubble-content]:bg-background [&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:text-foreground dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-input/30",        ghost:          "border-none *:data-[slot=bubble-content]:rounded-none *:data-[slot=bubble-content]:bg-transparent *:data-[slot=bubble-content]:p-0 [&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:text-foreground dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted/50",        destructive:          "*:data-[slot=bubble-content]:bg-destructive/10 *:data-[slot=bubble-content]:text-destructive dark:*:data-[slot=bubble-content]:bg-destructive/20 [&>[data-slot=bubble-content]:is(button,a):hover]:bg-destructive/20 dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-destructive/30",      },    },    defaultVariants: {      variant: "default",    },  })function Bubble({  variant = "default",  align = "start",  className,  ...props}: React.ComponentProps<"div"> &  VariantProps<typeof bubbleVariants> & {    align?: "start" | "end"  }) {  return (    <div      data-slot="bubble"      data-variant={variant}      data-align={align}      className={cn(bubbleVariants({ variant }), className)}      {...props}    />  )}function BubbleContent({  asChild = false,  className,  ...props}: React.ComponentProps<"div"> & {  asChild?: boolean}) {  const Comp = asChild ? Slot.Root : "div"  return (    <Comp      data-slot="bubble-content"      className={cn(        "w-fit max-w-full min-w-0 overflow-hidden rounded-xl border border-transparent px-3 py-2 text-sm leading-relaxed wrap-break-word group-data-[align=end]/bubble:self-end [button]:text-left [button,a]:transition-colors [button,a]:outline-none [button,a]:focus-visible:border-ring [button,a]:focus-visible:ring-3 [button,a]:focus-visible:ring-ring/50",        className      )}      {...props}    />  )}const bubbleReactionsVariants = cva(  "absolute z-10 flex w-fit shrink-0 items-center justify-center gap-1 rounded-full bg-muted px-1.5 py-0.5 text-sm ring-3 ring-card has-[button]:p-0",  {    variants: {      side: {        top: "top-0 -translate-y-3/4",        bottom: "bottom-0 translate-y-3/4",      },      align: {        start: "left-3",        end: "right-3",      },    },    defaultVariants: {      side: "bottom",      align: "end",    },  })function BubbleReactions({  side = "bottom",  align = "end",  className,  ...props}: React.ComponentProps<"div"> & {  align?: "start" | "end"  side?: "top" | "bottom"}) {  return (    <div      data-slot="bubble-reactions"      data-align={align}      data-side={side}      className={cn(bubbleReactionsVariants({ side, align }), className)}      {...props}    />  )}export { BubbleGroup, Bubble, BubbleContent, BubbleReactions }
    
  3. Make sure the foundation is in place

    Components read their colors, radius and surfaces from the tokens in app/globals.css. See Theming.

Usage#

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

Examples#

Alignment

This bubble is aligned to the start. This is the default alignment.
This bubble is aligned to the end. Use this for user messages.

Collapsible

How can I help you today?
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 control...

Group Demo

Can you tell me what's the issue?
You tell me!
It worked yesterday. You broke it!
Find the bug and fix it.
๐Ÿ‘€
Want me to diff yesterday's you against today's you? It's a bit embarrassing.
How can I help you today?

Popover

Run the build script.
Failed to run the command.

Reactions

I don't need tests, I know my code works.
Bold. Fine I'll add some tests. I'll let you know when they're done.
Tests passed on the first try. All 142 of them. Looking good!
Are you sure I can run this command?

Tooltip

Did you remove the stale route?
Yes, removed it from the registry.