Bubble
A chat bubble surface for displaying conversational content, with variants, alignment, grouping, and reactions.
Usage
Default
Bubble(align: :end) do
BubbleContent { "Hey there! what's up?" }
end
Conversation
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
Variants
Variants
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
Alignment
Start and end
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
Inside an end-aligned message
Message(align: :end) do
div(class: "flex w-full flex-col") do
Bubble do
BubbleContent { "Follows the message to the end." }
end
end
end
Reactions
Reactions
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
Group
Bubble group
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
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
With Tooltip
Reveal metadata on hover
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
With Popover
Surface details on demand
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
Installation
Using RubyUI CLI
Run the install command
rails g ruby_ui:component Bubble
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 group-data-[align=end]/message:self-end",
VARIANTS[@variant]
]
}
end
end
end
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-xl border border-transparent px-3 py-2 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/50"
}
end
end
end
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
render Docs::VisualCodeExample.new(title: "Inside an end-aligned Message", context: self) do
<<~RUBY
Message(align: :end) do
div(class: "flex w-full flex-col") do
Bubble do
BubbleContent { "Follows the message to the end." }
end
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
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
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
Components
| Component | Built using | Source |
|---|---|---|
Bubble | Phlex | |
BubbleContent | Phlex | |
BubbleDocs | Phlex | |
BubbleGroup | Phlex | |
BubbleReactions | Phlex |