{"name":"bubble","title":"Bubble","description":"Displays conversational content in a message bubble. Supports variants, alignment, grouping, reactions, and collapsible content.","installCommand":"bunx marko-ui add bubble -y","usageTags":"<Bubble>","importSnippet":"import Bubble from \"@/components/ui/bubble/bubble.marko\";\nimport BubbleContent from \"@/components/ui/bubble/bubble-content.marko\";\nimport BubbleReactions from \"@/components/ui/bubble/bubble-reactions.marko\";","usageSnippet":"<Bubble>\n  <BubbleContent>\n    I checked the registry output and removed the stale route.\n  </BubbleContent>\n  <BubbleReactions>\n    <span>👍</span>\n  </BubbleReactions>\n</Bubble>","tags":["Bubble","BubbleBubbleContent","BubbleBubbleGroup","BubbleBubbleReactions"],"parts":[],"props":[{"name":"align","type":"'end' | 'start'","required":false},{"name":"variant","type":"'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'muted' | 'tinted' | null | undefined","required":false,"default":"default"}],"events":[],"keyboard":[],"accessibilityNotes":["`Bubble` renders the presentational message surface only. Keep conversation-level semantics (roles, list structure) on the surrounding container.","Reactions render as a row of emoji read one glyph at a time by screen readers, with counters like \"+8\" announced as \"plus eight\". Group the row as a single image with a descriptive `aria-label` (e.g. `aria-label=\"Reactions: thumbs up, fire, and 8 more\"`) so it announces once — `role=\"img\"` also hides the individual emoji from assistive tech, so no `aria-hidden` is needed.","When reactions are interactive, render real buttons instead of `role=\"img\"` spans, and give icon-only buttons an `aria-label`.","When a bubble is clickable, render a real `<button>` or `<a>` inside `BubbleContent` (see the `Links and Buttons` example below) so it is focusable and exposes the correct role — `BubbleContent` ships a visible focus ring for interactive descendants, and the accessible name comes from the bubble text.","Bubble variants signal role and tone with color alone. Pair them with text, alignment, or icons — for a `destructive` bubble, keep the error context in the message text rather than relying on color."],"examples":[{"id":"bubble-demo","title":"Basic","source":"import Bubble from \"@marko-ui/shadcn/ui/bubble/bubble.marko\";\nimport BubbleContent from \"@marko-ui/shadcn/ui/bubble/bubble-content.marko\";\nimport BubbleGroup from \"@marko-ui/shadcn/ui/bubble/bubble-group.marko\";\nimport BubbleReactions from \"@marko-ui/shadcn/ui/bubble/bubble-reactions.marko\";\n\n<div class=\"flex w-full max-w-sm flex-col gap-8 py-12\">\n  <Bubble align=\"end\">\n    <BubbleContent>Hey there! what's up?</BubbleContent>\n  </Bubble>\n  <BubbleGroup>\n    <Bubble variant=\"muted\">\n      <BubbleContent>Hey! Want to see chat bubbles?</BubbleContent>\n    </Bubble>\n    <Bubble variant=\"muted\">\n      <BubbleContent>\n        I can group messages, switch sides, and keep the whole thread easy to scan.\n      </BubbleContent>\n      <BubbleReactions role=\"img\" aria-label=\"Reaction: thumbs up\">\n        <span>👍</span>\n      </BubbleReactions>\n    </Bubble>\n  </BubbleGroup>\n  <Bubble align=\"end\">\n    <BubbleContent>Sure. Hit me with your best demo.</BubbleContent>\n  </Bubble>\n  <Bubble variant=\"muted\">\n    <BubbleContent>\n      Yes. You are reading a demo that is demoing itself. Very meta. Very on-brand.\n    </BubbleContent>\n    <BubbleReactions role=\"img\" aria-label=\"Reactions: thumbs up, fire, eyes, and 2 more\">\n      <span>👍</span>\n      <span>🔥</span>\n      <span>👀</span>\n      <span>+2</span>\n    </BubbleReactions>\n  </Bubble>\n</div>","description":"Two conversation turns using `Bubble`, `BubbleGroup`, and `BubbleReactions`."},{"id":"bubble-variants","title":"Variants","source":"import Bubble from \"@marko-ui/shadcn/ui/bubble/bubble.marko\";\nimport BubbleContent from \"@marko-ui/shadcn/ui/bubble/bubble-content.marko\";\nimport BubbleReactions from \"@marko-ui/shadcn/ui/bubble/bubble-reactions.marko\";\n\n<div class=\"flex w-full max-w-sm flex-col gap-12 py-12\">\n  <Bubble>\n    <BubbleContent>This is the default primary bubble.</BubbleContent>\n  </Bubble>\n  <Bubble variant=\"secondary\" align=\"end\">\n    <BubbleContent>This is the secondary variant.</BubbleContent>\n  </Bubble>\n  <Bubble variant=\"muted\">\n    <BubbleContent>\n      This one is muted. It uses a lower emphasis color for the chat bubble.\n    </BubbleContent>\n    <BubbleReactions role=\"img\" aria-label=\"Reaction: thumbs up\">\n      <span>👍</span>\n    </BubbleReactions>\n  </Bubble>\n  <Bubble variant=\"tinted\" align=\"end\">\n    <BubbleContent>\n      This one is tinted. The tint is a softer color derived from the primary color.\n    </BubbleContent>\n  </Bubble>\n  <Bubble variant=\"outline\">\n    <BubbleContent>We can also use an outlined variant.</BubbleContent>\n  </Bubble>\n  <Bubble variant=\"destructive\" align=\"end\">\n    <BubbleContent>Or a destructive variant with a reaction.</BubbleContent>\n    <BubbleReactions role=\"img\" aria-label=\"Reaction: fire\">\n      <span>🔥</span>\n    </BubbleReactions>\n  </Bubble>\n  <Bubble variant=\"ghost\">\n    <BubbleContent>\n      Ghost bubbles work for assistant text, bold text, and other content that should not be\n      framed. This is perfect for assistant messages that should not have a frame and can take\n      the full width of the container. You can also render code in it. Ghost bubbles are full\n      width and can take the full width of the container.\n    </BubbleContent>\n  </Bubble>\n</div>","description":"Use `variant` (\"default\" | \"secondary\" | \"muted\" | \"tinted\" | \"outline\" | \"ghost\" | \"destructive\") to change the visual treatment of the bubble. A bubble sizes to its content, up to 80% of the container width — `ghost` removes the max-width so content can span the full row."},{"id":"bubble-alignment","title":"Alignment","source":"import Bubble from \"@marko-ui/shadcn/ui/bubble/bubble.marko\";\nimport BubbleContent from \"@marko-ui/shadcn/ui/bubble/bubble-content.marko\";\n\n<div class=\"flex w-full max-w-sm flex-col gap-8 py-12\">\n  <Bubble variant=\"muted\">\n    <BubbleContent>\n      This bubble is aligned to the start. This is the default alignment.\n    </BubbleContent>\n  </Bubble>\n  <Bubble align=\"end\">\n    <BubbleContent>\n      This bubble is aligned to the end. Use this for user messages.\n    </BubbleContent>\n  </Bubble>\n</div>","description":"Use `align` (\"start\" | \"end\") on `Bubble` to align it to the start or end of the conversation."},{"id":"bubble-group-demo","title":"Bubble Group","source":"import Bubble from \"@marko-ui/shadcn/ui/bubble/bubble.marko\";\nimport BubbleContent from \"@marko-ui/shadcn/ui/bubble/bubble-content.marko\";\nimport BubbleGroup from \"@marko-ui/shadcn/ui/bubble/bubble-group.marko\";\nimport BubbleReactions from \"@marko-ui/shadcn/ui/bubble/bubble-reactions.marko\";\n\n<div class=\"flex w-full max-w-sm flex-col gap-8 py-12\">\n  <Bubble variant=\"muted\">\n    <BubbleContent>Can you tell me what's the issue?</BubbleContent>\n  </Bubble>\n  <BubbleGroup>\n    <Bubble align=\"end\">\n      <BubbleContent>You tell me!</BubbleContent>\n    </Bubble>\n    <Bubble align=\"end\">\n      <BubbleContent>It worked yesterday. You broke it!</BubbleContent>\n    </Bubble>\n    <Bubble align=\"end\">\n      <BubbleContent>Find the bug and fix it.</BubbleContent>\n      <BubbleReactions role=\"img\" aria-label=\"Reactions: eyes\" align=\"start\">\n        <span>👀</span>\n      </BubbleReactions>\n    </Bubble>\n  </BubbleGroup>\n  <Bubble variant=\"muted\">\n    <BubbleContent>\n      Want me to diff yesterday's you against today's you? It's a bit embarrassing.\n    </BubbleContent>\n  </Bubble>\n</div>","description":"Use `BubbleGroup` to group consecutive bubbles from the same sender."},{"id":"bubble-link-button","title":"Links and Buttons","source":"import Bubble from \"@marko-ui/shadcn/ui/bubble/bubble.marko\";\nimport BubbleContent from \"@marko-ui/shadcn/ui/bubble/bubble-content.marko\";\nimport BubbleGroup from \"@marko-ui/shadcn/ui/bubble/bubble-group.marko\";\nimport { toast } from \"@marko-ui/shadcn/ui/toast/store.ts\";\nimport Toaster from \"@marko-ui/shadcn/ui/toast/toast.marko\";\n\n<div class=\"flex w-full max-w-sm flex-col gap-8 py-12\">\n  <Bubble variant=\"muted\">\n    <BubbleContent>How can I help you today?</BubbleContent>\n  </Bubble>\n  <BubbleGroup>\n    <Bubble variant=\"tinted\" align=\"end\">\n      <BubbleContent>\n        <button type=\"button\" class=\"text-left\" onClick() { toast.message(\"You clicked forgot password\"); }>\n          I forgot my password\n        </button>\n      </BubbleContent>\n    </Bubble>\n    <Bubble variant=\"tinted\" align=\"end\">\n      <BubbleContent>\n        <button\n          type=\"button\"\n          class=\"text-left\"\n          onClick() { toast.message(\"You clicked help with subscription\"); }\n        >\n          I need help with my subscription\n        </button>\n      </BubbleContent>\n    </Bubble>\n    <Bubble variant=\"tinted\" align=\"end\">\n      <BubbleContent>\n        <button\n          type=\"button\"\n          class=\"text-left\"\n          onClick() { toast.message(\"You clicked something else. Talk to a human.\"); }\n        >\n          Something else. Talk to a human.\n        </button>\n      </BubbleContent>\n    </Bubble>\n  </BubbleGroup>\n</div>\n<Toaster/>","description":"Turn a bubble into an interactive control by placing a real `<button>` or `<a>` inside `BubbleContent`. `BubbleContent` has no `render`/`asChild` prop — it always renders a `<div>` (Marko has no runtime slot-merge primitive), so the interactive element is a child rather than the bubble root."},{"id":"bubble-reactions","title":"Reactions","source":"import Bubble from \"@marko-ui/shadcn/ui/bubble/bubble.marko\";\nimport BubbleContent from \"@marko-ui/shadcn/ui/bubble/bubble-content.marko\";\nimport BubbleReactions from \"@marko-ui/shadcn/ui/bubble/bubble-reactions.marko\";\nimport Button from \"@marko-ui/shadcn/ui/button/button.marko\";\nimport { toast } from \"@marko-ui/shadcn/ui/toast/store.ts\";\nimport Toaster from \"@marko-ui/shadcn/ui/toast/toast.marko\";\n\n<div class=\"flex w-full max-w-sm flex-col gap-12 py-12\">\n  <Bubble variant=\"muted\" align=\"end\">\n    <BubbleContent>I don't need tests, I know my code works.</BubbleContent>\n    <BubbleReactions align=\"start\" role=\"img\" aria-label=\"Reactions: thumbs up, surprised\">\n      <span>👍</span>\n      <span>😮</span>\n    </BubbleReactions>\n  </Bubble>\n  <Bubble variant=\"muted\">\n    <BubbleContent>\n      Bold. Fine I'll add some tests. I'll let you know when they're done.\n    </BubbleContent>\n    <BubbleReactions role=\"img\" aria-label=\"Reactions: eyes, rocket, and 2 more\">\n      <span>👀</span>\n      <span>🚀</span>\n      <span>+2</span>\n    </BubbleReactions>\n  </Bubble>\n  <Bubble variant=\"default\" align=\"end\">\n    <BubbleContent>\n      Tests passed on the first try. All 142 of them. Looking good!\n    </BubbleContent>\n    <BubbleReactions side=\"top\" align=\"start\" role=\"img\" aria-label=\"Reactions: party popper, clapping hands\">\n      <span>🎉</span>\n      <span>👏</span>\n    </BubbleReactions>\n  </Bubble>\n  <Bubble variant=\"destructive\">\n    <BubbleContent>Are you sure I can run this command?</BubbleContent>\n    <BubbleReactions>\n      <Button type=\"button\" variant=\"ghost\" size=\"xs\" onClick() { toast.success(\"You clicked yes, running command...\"); }>\n        Yes, run it\n      </Button>\n    </BubbleReactions>\n  </Bubble>\n</div>\n<Toaster/>","description":"Use `BubbleReactions` for reactions or quick action buttons. Use `side` and `align` to position the row — reactions overlap the bubble edge, so leave vertical space between rows."},{"id":"bubble-collapsible","title":"Show More / Collapsible","source":"import Bubble from \"@marko-ui/shadcn/ui/bubble/bubble.marko\";\nimport BubbleContent from \"@marko-ui/shadcn/ui/bubble/bubble-content.marko\";\nimport Button from \"@marko-ui/shadcn/ui/button/button.marko\";\nimport Collapsible from \"@marko-ui/shadcn/ui/collapsible/collapsible.marko\";\nimport Icon from \"@marko-ui/shadcn/ui/icon/icon.marko\";\n\nstatic const text = `The accessibility review found two focus states that were visually too subtle in dark mode.\n\nI checked the dialog, menu, and drawer paths because each one renders focusable controls inside a layered surface.\n\nThe dialog and drawer are fine. The menu needs the hover and focus tokens split so keyboard focus stays visible when the pointer is not involved.\n\nI also recommend keeping the change in the style file instead of the primitive so the other themes can choose their own focus treatment later.`;\nstatic const previewLength = 180;\n\n<let/open=false/>\n<const/isLong=(text.length > previewLength)/>\n<const/preview=`${text.slice(0, previewLength)}...`/>\n\n<div class=\"flex w-full max-w-sm flex-col gap-8 py-12\">\n  <Bubble variant=\"muted\">\n    <BubbleContent>How can I help you today?</BubbleContent>\n  </Bubble>\n\n  <Bubble variant=\"muted\" align=\"end\">\n    <BubbleContent class=\"whitespace-pre-line\">\n      <Collapsible open=open openChange(next) { open = next; }>\n        <div>${open || !isLong ? text : preview}</div>\n        <if=isLong>\n          <@trigger|triggerProps|>\n            <Button type=\"button\" ...triggerProps variant=\"link\" class=\"gap-1 p-0 text-muted-foreground\">\n              ${open ? \"Show less\" : \"Show more\"}\n              <Icon name=\"ChevronDown\" class=`transition-transform ${open ? \"rotate-180\" : \"\"}`/>\n            </Button>\n          </@trigger>\n        </if>\n      </Collapsible>\n    </BubbleContent>\n  </Bubble>\n</div>","description":"Compose long bubble content with `Collapsible` for a show more / show less interaction.","essential":true},{"id":"bubble-tooltip","title":"Tooltip","source":"import Bubble from \"@marko-ui/shadcn/ui/bubble/bubble.marko\";\nimport BubbleContent from \"@marko-ui/shadcn/ui/bubble/bubble-content.marko\";\nimport BubbleReactions from \"@marko-ui/shadcn/ui/bubble/bubble-reactions.marko\";\nimport Button from \"@marko-ui/shadcn/ui/button/button.marko\";\nimport Icon from \"@marko-ui/shadcn/ui/icon/icon.marko\";\nimport Tooltip from \"@marko-ui/shadcn/ui/tooltip/tooltip.marko\";\n\n<div class=\"flex w-full max-w-sm flex-col gap-4 py-12\">\n  <Bubble variant=\"secondary\">\n    <BubbleContent>Did you remove the stale route?</BubbleContent>\n  </Bubble>\n  <Bubble align=\"end\">\n    <BubbleContent>Yes, removed it from the registry.</BubbleContent>\n    <BubbleReactions>\n\n      <Tooltip content=\"Read on Jan 5, 2026 at 4:32 PM\">\n        <@trigger|triggerProps|>\n          <Button type=\"button\" ...triggerProps variant=\"ghost\" size=\"icon-xs\" aria-label=\"Read on Jan 5, 2026 at 4:32 PM\">\n            <Icon name=\"Check\"/>\n          </Button>\n        </@trigger>\n      </Tooltip>\n    </BubbleReactions>\n  </Bubble>\n</div>","description":"Wrap a bubble control in `Tooltip` to reveal metadata on hover, such as when a message was read.","essential":true},{"id":"bubble-popover","title":"Popover","source":"import Bubble from \"@marko-ui/shadcn/ui/bubble/bubble.marko\";\nimport BubbleContent from \"@marko-ui/shadcn/ui/bubble/bubble-content.marko\";\nimport BubbleReactions from \"@marko-ui/shadcn/ui/bubble/bubble-reactions.marko\";\nimport Button from \"@marko-ui/shadcn/ui/button/button.marko\";\nimport Icon from \"@marko-ui/shadcn/ui/icon/icon.marko\";\nimport Popover from \"@marko-ui/shadcn/ui/popover/popover.marko\";\nimport PopoverHeader from \"@marko-ui/shadcn/ui/popover/header.marko\";\nimport PopoverTitle from \"@marko-ui/shadcn/ui/popover/title.marko\";\nimport PopoverDescription from \"@marko-ui/shadcn/ui/popover/description.marko\";\n\n<div class=\"flex w-full max-w-sm flex-col gap-4 py-12\">\n  <Bubble align=\"end\">\n    <BubbleContent>Run the build script.</BubbleContent>\n  </Bubble>\n  <Bubble variant=\"destructive\">\n    <BubbleContent>Failed to run the command.</BubbleContent>\n    <BubbleReactions>\n      <Popover>\n        <@trigger|triggerProps|>\n          <Button type=\"button\"\n            ...triggerProps\n            variant=\"ghost\"\n            size=\"icon-xs\"\n            aria-label=\"Show error details\"\n            class=\"aria-expanded:text-destructive\"\n          >\n            <Icon name=\"InfoIcon\"/>\n          </Button>\n        </@trigger>\n        <PopoverHeader>\n          <PopoverTitle class=\"text-sm\">Command failed with exit code 1</PopoverTitle>\n          <PopoverDescription class=\"text-sm\">\n            ENOENT: no such file or directory, open pnpm-lock.yaml\n          </PopoverDescription>\n        </PopoverHeader>\n      </Popover>\n    </BubbleReactions>\n  </Bubble>\n</div>","description":"Pair a bubble with `Popover` to surface more information on demand, such as a failed action's error message."}],"importInstallCommand":"bun add @marko-ui/shadcn marko-zag","nativeAttributes":"div","subcomponents":[{"name":"bubble-content","props":[],"nativeAttributes":"div"},{"name":"bubble-group","props":[],"nativeAttributes":"div"},{"name":"bubble-reactions","props":[{"name":"align","type":"'end' | 'start'","required":false},{"name":"side","type":"'bottom' | 'top'","required":false}],"nativeAttributes":"div"}],"composition":"`Bubble` renders the presentational message surface: place avatars, names,\ntimestamps, and message-level actions in the [`Message`](/docs/components/message)\ncomponent instead — `Bubble` is intentionally scoped to the bubble surface\nitself.\n\nCompose a bubble from `BubbleContent` and, optionally, `BubbleReactions`:\n\n```text\nBubble\n├── BubbleContent\n└── BubbleReactions\n```\n\nUse `BubbleGroup` to group consecutive bubbles from the same sender. The\n`align` prop stays on each `Bubble`, not on the group:\n\n```text\nBubbleGroup\n├── Bubble\n│   └── BubbleContent\n└── Bubble\n    └── BubbleContent\n```\n\n**Deviation from upstream:** upstream's `BubbleContent` accepts a `render`\nprop (Radix `Slot.Root`) to turn the content into a link or button by\nmerging its props onto an arbitrary child element. Marko 6 has no\n`asChild`/`Slot.Root` equivalent, and this port's other components (e.g.\n`button.marko`) already drop `asChild` for the same reason.\n`BubbleContent` here always\nrenders a `<div>`; put a real `<button>` or `<a>` inside the content body\nwhen you need an interactive bubble (the `Links and Buttons` example below\nshows the pattern)."}
