Bubble
A chat bubble surface for displaying conversational content, with variants, alignment, grouping, and reactions.
Usage
Default
Hey there! what's up?
Bubble(align: :end) do BubbleContent { "Hey there! what's up?" } end
Copied!
Copy failed!
Conversation
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.
div(class: "flex flex-col gap-8") do Bubble(align: :end) do BubbleContent { "Hey there! what's up?" } end BubbleGroup do Bubble(variant: :muted) do BubbleContent { "Hey! Want to see chat bubbles?" } end Bubble(variant: :muted) do BubbleContent { "I can group messages, switch sides, and keep the whole thread easy to scan." } BubbleReactions(role: "img", aria_label: "Reaction: thumbs up") do span { "👍" } end end end Bubble(align: :end) do BubbleContent { "Sure. Hit me with your best demo." } end end
Copied!
Copy failed!
Variants
Variants
Default
Secondary
Muted
Tinted
Outline
Ghost — unframed, full width for assistant text or markdown.
Destructive — something went wrong.
div(class: "flex flex-col gap-4 w-full") do Bubble(variant: :default) { BubbleContent { "Default" } } Bubble(variant: :secondary) { BubbleContent { "Secondary" } } Bubble(variant: :muted) { BubbleContent { "Muted" } } Bubble(variant: :tinted) { BubbleContent { "Tinted" } } Bubble(variant: :outline) { BubbleContent { "Outline" } } Bubble(variant: :ghost) { BubbleContent { "Ghost — unframed, full width for assistant text or markdown." } } Bubble(variant: :destructive) { BubbleContent { "Destructive — something went wrong." } } end
Copied!
Copy failed!
Alignment
Start and end
Aligned to the start (receiver).
Aligned to the end (sender).
div(class: "flex flex-col gap-4 w-full") do Bubble(align: :start, variant: :muted) do BubbleContent { "Aligned to the start (receiver)." } end Bubble(align: :end) do BubbleContent { "Aligned to the end (sender)." } end end
Copied!
Copy failed!
Reactions
Reactions
Reactions anchor to the bubble edge.
👍🔥👀+2
Place them on top and to the start too.
❤️
div(class: "flex flex-col gap-10 w-full py-6") do Bubble(variant: :muted) do BubbleContent { "Reactions anchor to the bubble edge." } BubbleReactions(role: "img", aria_label: "Reactions: thumbs up, fire, eyes, and 2 more") do span { "👍" } span { "🔥" } span { "👀" } span { "+2" } end end Bubble(align: :end) do BubbleContent { "Place them on top and to the start too." } BubbleReactions(side: :top, align: :start, role: "img", aria_label: "Reaction: heart") do span { "❤️" } end end end
Copied!
Copy failed!
Group
Bubble group
First message in the group.
Second one, tighter spacing.
Third, all stacked together.
BubbleGroup do Bubble(variant: :muted) { BubbleContent { "First message in the group." } } Bubble(variant: :muted) { BubbleContent { "Second one, tighter spacing." } } Bubble(variant: :muted) { BubbleContent { "Third, all stacked together." } } end
Copied!
Copy failed!
Link or button bubble
Interactive content
div(class: "flex flex-col gap-4 w-full") do Bubble(align: :end) do BubbleContent(as: :a, href: "#") { "Tap to open the link →" } end Bubble(variant: :outline) do BubbleContent(as: :button, type: "button") { "Retry sending" } end end
Copied!
Copy failed!
With Tooltip
Reveal metadata on hover
Read 9:41 AM
Delivered and read
Tooltip do TooltipTrigger(class: "w-fit") do Bubble(variant: :muted, class: "max-w-none") do BubbleContent { "Read 9:41 AM" } end end TooltipContent do Text { "Delivered and read" } end end
Copied!
Copy failed!
With Popover
Surface details on demand
Message failed to send
Delivery error
The recipient's inbox is full. Try again later.
Popover do PopoverTrigger do Bubble(variant: :destructive, class: "max-w-none") do BubbleContent { "Message failed to send" } end end PopoverContent(class: "w-64") do Text(weight: :semibold) { "Delivery error" } Text(size: :sm, class: "text-muted-foreground") { "The recipient's inbox is full. Try again later." } end end
Copied!
Copy failed!
Installation
Using RubyUI CLI
Run the install command
rails g ruby_ui:component Bubble
Copied!
Copy failed!
Manual installation
1
Add RubyUI::Bubble to app/components/ruby_ui/bubble/bubble.rb
# frozen_string_literal: true module RubyUI class Bubble < Base VARIANTS = { 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-secondary *:data-[slot=bubble-content]:text-secondary-foreground [&>[data-slot=bubble-content]:is(button,a):hover]:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)]", muted: "*:data-[slot=bubble-content]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:bg-[color-mix(in_oklch,var(--muted),var(--foreground)_5%)]", tinted: "*:data-[slot=bubble-content]:bg-[oklch(from_var(--primary)_0.93_calc(c*0.4)_h)] dark:*:data-[slot=bubble-content]:bg-[oklch(from_var(--primary)_0.3_calc(c*0.4)_h)] *:data-[slot=bubble-content]:text-foreground [&>[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]:bg-background *:data-[slot=bubble-content]:border-border [&>[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: "*: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 border-none", destructive: "*:data-[slot=bubble-content]:bg-destructive/10 dark:*:data-[slot=bubble-content]:bg-destructive/20 *:data-[slot=bubble-content]:text-destructive [&>[data-slot=bubble-content]:is(button,a):hover]:bg-destructive/20 dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-destructive/30" } def initialize(variant: :default, align: :start, **attrs) @variant = variant @align = align super(**attrs) end def view_template(&) div(**attrs, &) end private def default_attrs { data: {slot: "bubble", variant: @variant, align: @align}, class: [ "group/bubble relative flex w-fit min-w-0 flex-col gap-1 max-w-[80%] data-[align=end]:self-end data-[variant=ghost]:max-w-full", VARIANTS[@variant] ] } end end end
Copied!
Copy failed!
2
Add RubyUI::BubbleContent to app/components/ruby_ui/bubble/bubble_content.rb
# frozen_string_literal: true module RubyUI class BubbleContent < Base def initialize(as: :div, **attrs) @as = as super(**attrs) end def view_template(&) send(@as, **attrs, &) end private def default_attrs { data: {slot: "bubble-content"}, class: "w-fit max-w-full min-w-0 overflow-hidden wrap-break-word rounded-3xl border border-transparent px-3 py-2.5 text-sm leading-relaxed group-data-[align=end]/bubble:self-end [&:is(button,a)]:text-left [&:is(button,a)]:outline-none [&:is(button,a)]:transition-colors [&:is(button,a):focus-visible]:border-ring [&:is(button,a):focus-visible]:ring-3 [&:is(button,a):focus-visible]:ring-ring/30" } end end end
Copied!
Copy failed!
3
Add RubyUI::BubbleDocs to app/components/ruby_ui/bubble/bubble_docs.rb
# frozen_string_literal: true class Views::Docs::Bubble < Views::Base def view_template component = "Bubble" div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do render Docs::Header.new(title: "Bubble", description: "A chat bubble surface for displaying conversational content, with variants, alignment, grouping, and reactions.") Heading(level: 2) { "Usage" } render Docs::VisualCodeExample.new(title: "Default", context: self) do <<~RUBY Bubble(align: :end) do BubbleContent { "Hey there! what's up?" } end RUBY end render Docs::VisualCodeExample.new(title: "Conversation", context: self) do <<~RUBY div(class: "flex flex-col gap-8") do Bubble(align: :end) do BubbleContent { "Hey there! what's up?" } end BubbleGroup do Bubble(variant: :muted) do BubbleContent { "Hey! Want to see chat bubbles?" } end Bubble(variant: :muted) do BubbleContent { "I can group messages, switch sides, and keep the whole thread easy to scan." } BubbleReactions(role: "img", aria_label: "Reaction: thumbs up") do span { "👍" } end end end Bubble(align: :end) do BubbleContent { "Sure. Hit me with your best demo." } end end RUBY end Heading(level: 2) { "Variants" } render Docs::VisualCodeExample.new(title: "Variants", context: self) do <<~RUBY div(class: "flex flex-col gap-4 w-full") do Bubble(variant: :default) { BubbleContent { "Default" } } Bubble(variant: :secondary) { BubbleContent { "Secondary" } } Bubble(variant: :muted) { BubbleContent { "Muted" } } Bubble(variant: :tinted) { BubbleContent { "Tinted" } } Bubble(variant: :outline) { BubbleContent { "Outline" } } Bubble(variant: :ghost) { BubbleContent { "Ghost — unframed, full width for assistant text or markdown." } } Bubble(variant: :destructive) { BubbleContent { "Destructive — something went wrong." } } end RUBY end Heading(level: 2) { "Alignment" } render Docs::VisualCodeExample.new(title: "Start and end", context: self) do <<~RUBY div(class: "flex flex-col gap-4 w-full") do Bubble(align: :start, variant: :muted) do BubbleContent { "Aligned to the start (receiver)." } end Bubble(align: :end) do BubbleContent { "Aligned to the end (sender)." } end end RUBY end Heading(level: 2) { "Reactions" } render Docs::VisualCodeExample.new(title: "Reactions", context: self) do <<~RUBY div(class: "flex flex-col gap-10 w-full py-6") do Bubble(variant: :muted) do BubbleContent { "Reactions anchor to the bubble edge." } BubbleReactions(role: "img", aria_label: "Reactions: thumbs up, fire, eyes, and 2 more") do span { "👍" } span { "🔥" } span { "👀" } span { "+2" } end end Bubble(align: :end) do BubbleContent { "Place them on top and to the start too." } BubbleReactions(side: :top, align: :start, role: "img", aria_label: "Reaction: heart") do span { "❤️" } end end end RUBY end Heading(level: 2) { "Group" } render Docs::VisualCodeExample.new(title: "Bubble group", context: self) do <<~RUBY BubbleGroup do Bubble(variant: :muted) { BubbleContent { "First message in the group." } } Bubble(variant: :muted) { BubbleContent { "Second one, tighter spacing." } } Bubble(variant: :muted) { BubbleContent { "Third, all stacked together." } } end RUBY end Heading(level: 2) { "Link or button bubble" } render Docs::VisualCodeExample.new(title: "Interactive content", context: self) do <<~RUBY div(class: "flex flex-col gap-4 w-full") do Bubble(align: :end) do BubbleContent(as: :a, href: "#") { "Tap to open the link →" } end Bubble(variant: :outline) do BubbleContent(as: :button, type: "button") { "Retry sending" } end end RUBY end Heading(level: 2) { "With Tooltip" } render Docs::VisualCodeExample.new(title: "Reveal metadata on hover", context: self) do <<~RUBY Tooltip do TooltipTrigger(class: "w-fit") do Bubble(variant: :muted, class: "max-w-none") do BubbleContent { "Read 9:41 AM" } end end TooltipContent do Text { "Delivered and read" } end end RUBY end Heading(level: 2) { "With Popover" } render Docs::VisualCodeExample.new(title: "Surface details on demand", context: self) do <<~RUBY Popover do PopoverTrigger do Bubble(variant: :destructive, class: "max-w-none") do BubbleContent { "Message failed to send" } end end PopoverContent(class: "w-64") do Text(weight: :semibold) { "Delivery error" } Text(size: :sm, class: "text-muted-foreground") { "The recipient's inbox is full. Try again later." } end end RUBY end render Components::ComponentSetup::Tabs.new(component_name: component) # components render Docs::ComponentsTable.new(component_files(component)) end end end
Copied!
Copy failed!
4
Add RubyUI::BubbleGroup to app/components/ruby_ui/bubble/bubble_group.rb
# frozen_string_literal: true module RubyUI class BubbleGroup < Base def view_template(&) div(**attrs, &) end private def default_attrs { data: {slot: "bubble-group"}, class: "flex min-w-0 flex-col gap-2" } end end end
Copied!
Copy failed!
5
Add RubyUI::BubbleReactions to app/components/ruby_ui/bubble/bubble_reactions.rb
# frozen_string_literal: true module RubyUI class BubbleReactions < Base SIDES = { top: "top-0 -translate-y-3/4", bottom: "bottom-0 translate-y-3/4" } ALIGNS = { start: "left-3", end: "right-3" } def initialize(side: :bottom, align: :end, **attrs) @side = side @align = align super(**attrs) end def view_template(&) div(**attrs, &) end private def default_attrs { data: {slot: "bubble-reactions", side: @side, align: @align}, class: [ "absolute z-10 flex w-fit items-center justify-center rounded-full ring-3 ring-card bg-muted shrink-0 gap-1 px-1.5 py-0.5 has-[button]:p-0 text-sm", SIDES[@side], ALIGNS[@align] ] } end end end
Copied!
Copy failed!
Components
| Component | Built using | Source |
|---|---|---|
Bubble | Phlex | |
BubbleContent | Phlex | |
BubbleDocs | Phlex | |
BubbleGroup | Phlex | |
BubbleReactions | Phlex |