[Feature] Bubble: match shadcn (Base UI + vega)

Merged
CI
2d ago
+214-5

Restyles Bubble to match shadcn's Base UI + vega Bubble. After this PR the shadcn audit finds no Look, HTML, Props or ARIA differences.

Bubble, light
Bubble, dark
Bubble button focus, light
Bubble button focus, dark

Changes

  • BubbleContent uses rounded-xl py-2, as in vega: 14px corners and 8px vertical padding. It used rounded-3xl py-2.5 (24px and 10px), so bubbles looked like pills and were 4px taller.
  • When BubbleContent renders as a button or link, its keyboard focus ring is ring-ring/50, as in vega. It was ring-ring/30, which was harder to see.
  • Bubble gets vega's group-data-[align=end]/message:self-end, so a Bubble inside Message(align: :end) moves to the end on its own. In the usual Message > MessageContent > Bubble nothing changes, because MessageContent already does this. It shows when the Bubble sits in another wrapper, like the "Aligned by the bubble itself." case in the screenshots, which stayed at the start on main.
  • Bubble is now part of the shadcn audit (audit/component_map/bubble.yml, audit/fixtures/bubble/). The fixture covers all seven variants, reactions on both sides, a button bubble, and two bubbles inside an end-aligned Message (one in MessageContent, one in a plain column).

Possible breaking change for apps that copied the component: bubbles get squarer corners and are 4px shorter. To keep the old look, pass class: "rounded-3xl py-2.5" to BubbleContent.

Test steps

  1. cd gem && bundle exec rake
  2. cd docs && bin/dev, open /docs/bubble and /docs/message: check the variants, alignment and reactions in light and dark.
  3. On /docs/bubble, press Tab until a button bubble has focus: the ring should be clearly visible.
shadcn audit, main
shadcn audit  ruby-ui/ruby_ui
  base 27edd81c  head 27edd81c  shadcn c0d63c33
⚠️ Environment  font: "Inter Variable", "system-ui", -apple-system, "system-ui", "Segoe UI", Roboto, "Helvetica Neue", Arial, "sans-serif" vs "Inter Variable", sans-serif (light)
⚠️ Environment  font: "Inter Variable", "system-ui", -apple-system, "system-ui", "Segoe UI", Roboto, "Helvetica Neue", Arial, "sans-serif" vs "Inter Variable", sans-serif (dark)
⚠️ Environment  RubyUI preview boot: the global Ruby 3.4.7 on macmini has rdoc 8.1.0, whose RubyGems plugin activates date 3.6.0 before Bundler; the throwaway preview lockfile was bumped from date 3.5.1 to 3.6.0 to boot. No effect on rendering.
✅ Theme  neutral tokens match shadcn
Bubble  (ref: bases/base/ui/bubble.tsx + style-vega.css @ c0d63c33)
  ❌ Look       border-bottom-left-radius: 24px; border-bottom-right-radius: 24px; border-top-left-radius: 24px; border-top-right-radius: 24px; padding-bottom: 10px; padding-top: 10px
               vega: border-bottom-left-radius: 14px; border-bottom-right-radius: 14px; border-top-left-radius: 14px; border-top-right-radius: 14px; padding-bottom: 8px; padding-top: 8px
               BubbleContent — closed/light, closed/dark. RubyUI rounded-3xl py-2.5 vs vega rounded-xl py-2 (cn-bubble-content). rounded-xl = calc(var(--radius) * 1.4) = 14px with --radius 0.625rem. Requested: use rounded-xl py-2 on BubbleContent.
               ↳ style-vega.css:1464
  ⏸ Look       properties: font-family, height
               BubbleGroup, Bubble, BubbleContent, BubbleReactions — explained by environment diff: font. Both sides resolve to Inter Variable first; only the fallback stack differs. Heights also differ by the py-2.5 vs py-2 BubbleContent padding above. Kept as is: environment (fallback font stack); height follows the BubbleContent padding fix.
  ✅ ARIA       BubbleGroup, Bubble, BubbleContent, BubbleReactions: accessibility tree matches
               ↳ accessibility tree (shadcn-audit collect)
  ❌ Look       BubbleContent (as: :button) focus-visible: ring-3 ring-ring/30 → box-shadow oklab(0.708 0 0 / 0.3) light, oklab(0.556 0 0 / 0.3) dark
               vega: [button,a]:focus-visible:ring-ring/50 → oklab(0.708 0 0 / 0.5) light, oklab(0.556 0 0 / 0.5) dark
               Keyboard focus (Tab) on the button bubble, read after the transition settles; border-color matches (border-ring). Screens: screens/bubble-focus-{rubyui,shadcn}-{light,dark}.png. Requested: use ring-ring/50 on the BubbleContent focus ring.
               ↳ style-vega.css:1464
  ❌ Look       Bubble: no group-data-[align=end]/message:self-end
               vega: cn-bubble: group-data-[align=end]/message:self-end
               Browser, fixture case Message(align: :end) > div.flex.flex-col.w-full > Bubble: the RubyUI bubble stays at the start (222px gap on the right), shadcn moves it to the end (0px). In Message > MessageContent both sides already end-align, because MessageContent (like vega cn-message-content, style-vega.css:1758) pushes slotted children to the end.
               ↳ style-vega.css:1460
  ✅ Look       BubbleGroup, Bubble, BubbleReactions (side top/bottom, align start/end) and the seven variant backgrounds/text colors
               Variant class strings in Bubble::VARIANTS are identical to cn-bubble-variant-* (style-vega.css:1467-1493); hover states not driven.
               ↳ computed styles (shadcn-audit collect)
  ✅ HTML       div[data-slot=bubble-group] > div[data-slot=bubble][data-variant][data-align] > div|button[type=button][data-slot=bubble-content] + div[data-slot=bubble-reactions][data-side][data-align]
               Twelve bubbles (two inside an end-aligned Message), same data-variant/data-align/data-side on each.
               ↳ rendered DOM (both fixtures)
  ✅ Props      Bubble variant:, align:; BubbleContent as:; BubbleReactions side:, align:
               BubbleContent render={<button />} maps to as: :button (tag only; other attributes pass through **attrs). Defaults match: variant default, align start; reactions side bottom, align end.
               ↳ bases/base/ui/bubble.tsx:37
  ✅ Behavior   button bubble reachable by Tab, :focus-visible applies
               Static component; no JS on either side.
               ↳ browser run (Playwright, 1280×800)
  ✅ Native     no Stimulus controller; interactive content is a native <button type=button> — native
               ↳ https://developer.mozilla.org/pt-BR/docs/Web/HTML/Element/button
  ✅ Spec       button bubble shows a 3px focus ring
               Both meet 2.4.7 Focus Visible; shadcn side checked first.
               ↳ https://www.w3.org/TR/WCAG22/#focus-visible
shadcn audit, this PR
shadcn audit  ruby-ui/ruby_ui
  base 27edd81c  head ad21ba8e  shadcn c0d63c33
⚠️ Environment  font: "Inter Variable", "system-ui", -apple-system, "system-ui", "Segoe UI", Roboto, "Helvetica Neue", Arial, "sans-serif" vs "Inter Variable", sans-serif (light)
⚠️ Environment  font: "Inter Variable", "system-ui", -apple-system, "system-ui", "Segoe UI", Roboto, "Helvetica Neue", Arial, "sans-serif" vs "Inter Variable", sans-serif (dark)
⚠️ Environment  RubyUI preview boot: the global Ruby 3.4.7 on macmini has rdoc 8.1.0, whose RubyGems plugin activates date 3.6.0 before Bundler; the throwaway preview lockfile was bumped from date 3.5.1 to 3.6.0 to boot. No effect on rendering.
✅ Theme  neutral tokens match shadcn
Bubble  (ref: bases/base/ui/bubble.tsx + style-vega.css @ c0d63c33)
  ⏸ Look       properties: font-family
               BubbleGroup, Bubble, BubbleContent, BubbleReactions — explained by environment diff: font. Both sides resolve to Inter Variable first; only the fallback stack differs. Kept as is: environment.
  ✅ ARIA       BubbleGroup, Bubble, BubbleContent, BubbleReactions: accessibility tree matches
               ↳ accessibility tree (shadcn-audit collect)
  ✅ Look       BubbleContent rounded-xl py-2 (14px radius, 8px block padding); focus-visible ring-ring/50 → oklab(0.708 0 0 / 0.5) light, oklab(0.556 0 0 / 0.5) dark
               Computed styles closed light/dark, and keyboard focus (Tab) after the transition settles.
               ↳ style-vega.css:1464
  ✅ Look       Bubble group-data-[align=end]/message:self-end; in Message(align: :end) > div.flex.flex-col.w-full the bubble ends at the edge (0px gap), and in Message > MessageContent too
               New fixture cases, light and dark. On main the plain-column case leaves a 222px gap.
               ↳ style-vega.css:1460
  ✅ Look       BubbleGroup, Bubble, BubbleReactions (side top/bottom, align start/end) and the seven variant backgrounds/text colors
               Variant class strings in Bubble::VARIANTS are identical to cn-bubble-variant-* (style-vega.css:1467-1493); hover states not driven.
               ↳ computed styles (shadcn-audit collect)
  ✅ HTML       div[data-slot=bubble-group] > div[data-slot=bubble][data-variant][data-align] > div|button[type=button][data-slot=bubble-content] + div[data-slot=bubble-reactions][data-side][data-align]
               Twelve bubbles (two inside an end-aligned Message), same data-variant/data-align/data-side on each.
               ↳ rendered DOM (both fixtures)
  ✅ Props      Bubble variant:, align:; BubbleContent as:; BubbleReactions side:, align:
               BubbleContent render={<button />} maps to as: :button (tag only; other attributes pass through **attrs). Defaults match: variant default, align start; reactions side bottom, align end.
               ↳ bases/base/ui/bubble.tsx:37
  ✅ Behavior   button bubble reachable by Tab, :focus-visible applies
               Static component; no JS on either side.
               ↳ browser run (Playwright, 1280×800)
  ✅ Native     no Stimulus controller; interactive content is a native <button type=button> — native
               ↳ https://developer.mozilla.org/pt-BR/docs/Web/HTML/Element/button
  ✅ Spec       button bubble shows a 3px focus ring
               Both meet 2.4.7 Focus Visible; shadcn side checked first.
               ↳ https://www.w3.org/TR/WCAG22/#focus-visible

🤖 Generated with Claude Code


Summary by cubic

Restyles Bubble to match shadcn's Base UI + vega Bubble, so the shadcn audit finds no Look, HTML, Props or ARIA differences.

  • BubbleContent uses rounded-xl py-2 instead of rounded-3xl py-2.5: bubbles get 14px corners and 8px vertical padding instead of 24px and 10px.
  • The keyboard focus ring on button/link BubbleContent is now ring-ring/50 instead of ring-ring/30, making it clearly visible.
  • Bubble now end-aligns itself inside Message(align: :end) via group-data-[align=end]/message:self-end, so it works even when not nested in MessageContent.
  • Adds Bubble to the shadcn audit with fixtures covering all seven variants, reactions, a button bubble, and end-aligned messages.
  • Documents the end-aligned Message case under Alignment and adds a test for it.

Migration
Apps that copied the component can keep the old pill look by passing class: "rounded-3xl py-2.5" to BubbleContent.