Skip to content
Brok UI

Loading…

No results

Chat Bubble

Open source

A chat bubble for a single sender turn with an optional avatar and name label; supports user, other, and system roles.

Version
v2.1.2
Stability
stable
License
MIT
Related
Chat Thread
AI Prompt
Avatar

Preview

You
Is the Q3 launch still on track?
Support
Yes — the rollout is staged behind a flag and we are on schedule.
Assistant
The billing revamp ships first, then the new onboarding. Both are behind flags, so we can revert in one click.
Support
Conversation summarised to save space.
previews.components.chat-bubble.default.blade.php Blade
<div class="mx-auto flex w-full max-w-md flex-col gap-4">
    <x-ui.chat-bubble role="user" name="You">Is the Q3 launch still on track?</x-ui.chat-bubble>
    <x-ui.chat-bubble role="other" name="Support">Yes — the rollout is staged behind a flag and we are on schedule.</x-ui.chat-bubble>
    {{-- An assistant reply arriving: the text streams in once, with a caret. --}}
    <x-ui.chat-bubble role="other" name="Assistant" stream>The billing revamp ships first, then the new onboarding. Both are behind flags, so we can revert in one click.</x-ui.chat-bubble>
    {{-- The other side is typing. --}}
    <x-ui.chat-bubble role="other" name="Support" typing />
    <x-ui.chat-bubble role="system">Conversation summarised to save space.</x-ui.chat-bubble>
</div>

Installation

terminal
php artisan ui:add chat-bubble

Registry contract

php artisan ui:add chat-bubble writes only the files below. The CLI validates each file hash before writing and asks before it replaces a local change, unless you pass --force.

  • blade resources/views/components/ui/chat-bubble.blade.php
Registry dependencies
None — installs on its own.
Packages
composer: jml/brok:^0.2

Use with AI

A brief for your coding agent: install command, usage, props, guidance and the rules. Copy it, or open a prompt about this component in an assistant.

chat-bubble.md
# Brok UI: Chat Bubble (`chat-bubble`)

A chat bubble for a single sender turn with an optional avatar and name label; supports user, other, and system roles.

Brok UI is a Laravel Blade component registry. Installed components are plain Blade files the app owns.

## Install

```bash
php artisan ui:add chat-bubble
```

## Usage

```blade
<div class="mx-auto flex w-full max-w-md flex-col gap-4">
    <x-ui.chat-bubble role="user" name="You">Is the Q3 launch still on track?</x-ui.chat-bubble>
    <x-ui.chat-bubble role="other" name="Support">Yes — the rollout is staged behind a flag and we are on schedule.</x-ui.chat-bubble>
    {{-- An assistant reply arriving: the text streams in once, with a caret. --}}
    <x-ui.chat-bubble role="other" name="Assistant" stream>The billing revamp ships first, then the new onboarding. Both are behind flags, so we can revert in one click.</x-ui.chat-bubble>
    {{-- The other side is typing. --}}
    <x-ui.chat-bubble role="other" name="Support" typing />
    <x-ui.chat-bubble role="system">Conversation summarised to save space.</x-ui.chat-bubble>
</div>
```

## Props

- `role` (string, default `user`)
- `name` (mixed|null, default `null`)
- `avatar` (mixed|null, default `null`)
- `typing` (bool, default `false`)
- `stream` (bool, default `false`)

## Use when

- Use to group related content into scannable units for browsing, prioritization, or rearrangement.
- Rendering one turn in a conversation, aligned and coloured by who sent it.
- Showing that the other party is typing, or streaming a reply in as it arrives.

## Avoid when

- Do not use card-like structures when dense row-by-row comparison would be clearer in a list or table.
- Laying out a full conversation; chat-thread stacks the bubbles.
- Displaying a notification or toast; those have their own primitives.

## Anti-patterns

- Turning every object into a card

## Rules

- Use the `<brok:chat-bubble>` tag (or `<x-ui.chat-bubble>`) in Blade; do not rewrite the component.
- Prefer the documented props and variants over utility-class overrides; when a utility must win, use the `!` important modifier.
- Use semantic design tokens (`bg-primary`, `text-muted-foreground`), never raw colour utilities.
- Keep the `data-slot` attributes; they are the styling and test hooks.

## Links

- Docs: https://brokui.dev/docs/components/chat-bubble
- Registry JSON (files, props, contract): https://brokui.dev/r/open/chat-bubble.json

Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.

API

manifest knowledge + registry-derived coverage

Props

Props accepted by this component: name, type, default value and description.
Prop Type Default Description
role string user
name mixed | null null
avatar mixed | null null
typing bool false
stream bool false

Slots

  • default —

Data slots

Stable hooks for CSS overrides and browser tests.

chat-bubble chat-bubble-stream chat-bubble-typing

Behavior

  • role selects user, other or system alignment and colour, and name and avatar render beside the bubble.
  • typing replaces the content with an animated indicator; stream reveals the content progressively where motion is allowed.
  • Normalizes unknown roles to user, mirrors user/other alignment under RTL, and renders system turns as centered notices.
  • Declares registry capability flags: a11y, responsive, rtl, darkMode, localized.

Guidance

Card-based organization

Organize browsable objects into scannable units.

Use when

  • Use to group related content into scannable units for browsing, prioritization, or rearrangement.
  • Rendering one turn in a conversation, aligned and coloured by who sent it.
  • Showing that the other party is typing, or streaming a reply in as it arrives.

Avoid when

  • Do not use card-like structures when dense row-by-row comparison would be clearer in a list or table.
  • Laying out a full conversation; chat-thread stacks the bubbles.
  • Displaying a notification or toast; those have their own primitives.

Use instead

  • Table for exact comparison
  • List for compact scanning

Anti-patterns

  • Turning every object into a card
Anatomy
root chat-bubble-typing chat-bubble-stream
Theming hooks
chat-bubble chat-bubble-stream chat-bubble-typing

Accessibility

WCAG 2.2 AA Keyboard focus-visible RTL-ready Localized labels Dark mode
Keyboard
managed
Focus
none
  • The typing indicator is a role="status" region with a translated name, so it is announced rather than seen only.
  • Motion on the typing and streaming states stops under prefers-reduced-motion.
  • Semantic HTML and a stable data-slot attribute for styling and scripting hooks.
  • Focus-visible rings use the ring token, so keyboard focus is always visible.
  • Disabled and invalid states are conveyed to assistive tech, not by color alone.
  • Targets WCAG 2.2 AA; verify contrast in light, dark, admin and customer surfaces in the preview.
  • Labels go through __() and layout uses logical properties (ms-*, text-start), so it mirrors under dir="rtl" — flip the preview to RTL to confirm.
  • Dark mode uses the same semantic tokens under the dark class; high contrast follows forced-color system tokens.

Livewire

Needs wire:key

Add a stable wire:key when Livewire can reorder this interactive component.

livewire-component.blade.php Blade
<div wire:key="chat-bubble-{{ $record->id }}">
    <div class="mx-auto flex w-full max-w-md flex-col gap-4">
        <x-ui.chat-bubble role="user" name="You">Is the Q3 launch still on track?</x-ui.chat-bubble>
        <x-ui.chat-bubble role="other" name="Support">Yes — the rollout is staged behind a flag and we are on schedule.</x-ui.chat-bubble>
        {{-- An assistant reply arriving: the text streams in once, with a caret. --}}
        <x-ui.chat-bubble role="other" name="Assistant" stream>The billing revamp ships first, then the new onboarding. Both are behind flags, so we can revert in one click.</x-ui.chat-bubble>
        {{-- The other side is typing. --}}
        <x-ui.chat-bubble role="other" name="Support" typing />
        <x-ui.chat-bubble role="system">Conversation summarised to save space.</x-ui.chat-bubble>
    </div>
</div>

Source

The exact, editable file ui:add writes into your app. Previews render this same code; there are no preview-only components.

resources/views/components/ui/chat-bubble.blade.php Blade
@props([
    'role' => 'user',
    'name' => null,
    'avatar' => null,
    // The sender is typing: the bubble shows three pulsing dots instead of text.
    'typing' => false,
    // Stream the text in once, a few characters at a time with a caret, the
    // way an assistant reply arrives; reduced motion shows it at once.
    'stream' => false,
])

@php
    // Normalize role to one of the three supported variants.
    $role = in_array($role, ['user', 'other', 'system'], true) ? $role : 'user';
    $isUser = $role === 'user';
    $isSystem = $role === 'system';

    // Build initials for the avatar fallback from the provided name.
    $initials = null;
    if ($name) {
        $parts = preg_split('/\s+/', trim((string) $name));
        $initials = collect($parts)
            ->filter()
            ->take(2)
            ->map(fn ($p) => mb_strtoupper(mb_substr($p, 0, 1)))
            ->implode('');
    }

    // System turns render as a centered, subtle notice rather than a sided bubble.
    if ($isSystem) {
        $rowClasses = 'flex w-full justify-center';
    } else {
        // User turns flow from the end (mirrored in RTL); other senders from the start.
        $rowClasses = 'flex w-full items-end gap-2 '.($isUser ? 'flex-row-reverse' : 'flex-row');
    }

    if ($isSystem) {
        $bubbleClasses = 'max-w-[90%] rounded-full bg-accent px-3 py-1 text-center text-xs text-muted-foreground';
    } elseif ($isUser) {
        // Flatten the end (trailing) corner toward the avatar/edge.
        $bubbleClasses = 'max-w-[80%] rounded-md rounded-ee-sm bg-primary px-4 py-2 text-sm text-primary-foreground';
    } else {
        // Flatten the start (leading) corner toward the avatar/edge.
        $bubbleClasses = 'max-w-[80%] rounded-md rounded-es-sm bg-muted px-4 py-2 text-sm text-foreground';
    }
@endphp

<div
    data-slot="chat-bubble"
    data-role="{{ $role }}"
    {{ $attributes->merge(['class' => $rowClasses]) }}
>
    @unless ($isSystem)
        @if ($avatar || $initials)
            <div
                aria-hidden="true"
                class="flex size-8 shrink-0 select-none items-center justify-center overflow-hidden rounded-full bg-accent text-xs font-medium text-muted-foreground"
            >
                @if ($avatar)
                    <img src="{{ $avatar }}" alt="" class="size-full object-cover" />
                @else
                    {{ $initials }}
                @endif
            </div>
        @endif
    @endunless

    <div class="flex min-w-0 flex-col gap-1 {{ $isUser ? 'items-end' : 'items-start' }}">
        @if ($name && ! $isSystem)
            <span class="px-1 text-xs font-medium text-muted-foreground">{{ $name }}</span>
        @endif

        @if ($typing)
            <div class="{{ $bubbleClasses }}" data-slot="chat-bubble-typing" role="status" aria-label="{{ $name ? __(':name is typing', ['name' => $name]) : __('Typing') }}">
                <span class="flex h-5 items-center gap-1" aria-hidden="true">
                    @foreach ([0, 160, 320] as $delay)
                        <span class="size-2 shrink-0 rounded-full bg-current opacity-50 motion-safe:animate-[ui-ai-typing-bounce_1.2s_ease-in-out_infinite]" style="animation-delay: {{ $delay }}ms"></span>
                    @endforeach
                </span>
            </div>
        @elseif ($stream)
            <div
                class="{{ $bubbleClasses }}"
                data-slot="chat-bubble-stream"
                x-data="{
                    text: '',
                    shown: '',
                    done: false,
                    init() {
                        this.text = this.$refs.source.textContent.trim();
                        if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
                            this.shown = this.text;
                            this.done = true;
                            return;
                        }
                        let i = 0;
                        const tick = () => {
                            i += 1 + Math.floor(Math.random() * 3);
                            this.shown = this.text.slice(0, i);
                            if (i >= this.text.length) {
                                this.done = true;
                                return;
                            }
                            setTimeout(tick, 28);
                        };
                        tick();
                    },
                }"
            >
                {{-- The full text stays in the DOM for assistive tech; sighted
                     readers watch it arrive. --}}
                <span x-ref="source" class="sr-only">{{ $slot }}</span>
                <span aria-hidden="true" class="whitespace-pre-line" x-text="shown"></span><span x-show="!done" aria-hidden="true" class="ms-0.5 inline-block h-[1em] w-px translate-y-[0.15em] bg-current motion-safe:animate-[ui-caret-blink_1s_steps(2)_infinite]"></span>
            </div>
        @else
            <div class="{{ $bubbleClasses }}">
                {{ $slot }}
            </div>
        @endif
    </div>
</div>

Ownership & lifecycle

Owner, release state, review evidence and adoption for this item.
Owner
Platform UI (@JoshJML)
Current version
2.1.2
Status
Stable
License
open
Accessibility reviewed
No review date recorded
Last breaking change
No date recorded
Deprecation
Not deprecated
Contract
v6
Foundation
≥ 1.0.0