# Conversation

Auto-following conversation transcript with empty and jump-to-latest states.

- Package: `@mielui/svelte`
- Component version: `1.1.1`
- Depends on Mielui components: _internal/utils, scroll-area
- Shared utilities: transition, hugeicons-icon, utils.cn, utils.createContext, utils.pressable

## Install

~~~~sh
pnpm dlx @mielui/svelte add conversation
~~~~

## API

This reference is generated at build time from the component manifest, public `index.ts`, and documentation examples below. Changes to those source files are reflected here in the published Markdown. Standard Svelte and HTML attributes accepted by the exported prop types are supported.


### Content

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| transcriptClass | string \| undefined | - | No | No | Classes applied to the inner transcript stack rather than the scroll viewport. |
| class | (string & (ClassValue \| null)) \| undefined | - | No | No | Classes added to the element. |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Empty

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| icon | Snippet<[]> \| undefined | - | No | No | Icon shown above the title. |
| title | string \| undefined | - | No | No | Heading of the empty state. |
| description | string \| undefined | - | No | No | Text under the heading. |
| action | Snippet<[]> \| undefined | - | No | No | Action shown under the text. |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |
| class | (string & (ClassValue \| null)) \| undefined | - | No | No | Classes added to the element. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Root

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| labels | ConversationLabels \| undefined | - | No | No | Overrides the built-in text and accessible names. Every key is optional and English is the fallback. |
| follow | boolean \| undefined | true | No | Yes | Whether new content should keep the transcript pinned to its latest message. |
| threshold | number \| undefined | 80 | No | No | Distance from the bottom, in pixels, that still counts as following. |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |
| class | (string & (ClassValue \| null)) \| undefined | - | No | No | Classes added to the element. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### ScrollButton

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| class | string \| undefined | - | No | No | Classes added to the element. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Public types

~~~~ts
import type { DefaultProps } from '@mielui/svelte/utils';
import type { Snippet } from 'svelte';
import type { HTMLAttributes, HTMLButtonAttributes } from 'svelte/elements';
import Root from './conversation.svelte';
import Content from './conversation-content.svelte';
import Empty from './conversation-empty.svelte';
import ScrollButton from './conversation-scroll-button.svelte';

export type ConversationLabels = {
    /** Accessible name of the transcript when Content has no `aria-label`. */
    transcript?: string;
    /** Accessible name of the scroll-to-latest button. */
    scrollToBottom?: string;
    /** Heading of the empty state when Empty has no `title`. */
    emptyTitle?: string;
    /** Text of the empty state when Empty has no `description`. */
    emptyDescription?: string;
};

export type ConversationRootProps = {
    /**
     * Overrides the built-in text and accessible names. Every key is optional and English is the
     * fallback.
     */
    labels?: ConversationLabels;
    /** Whether new content should keep the transcript pinned to its latest message. */
    follow?: boolean;
    /** Distance from the bottom, in pixels, that still counts as following. */
    threshold?: number;
    /** Content rendered inside. */
    children?: Snippet;
} & DefaultProps &
    Omit<HTMLAttributes<HTMLDivElement>, 'children'>;

export type ConversationContentProps = {
    /** Classes applied to the inner transcript stack rather than the scroll viewport. */
    transcriptClass?: string;
} & DefaultProps &
    Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'role' | 'aria-live' | 'aria-relevant'>;

export type ConversationEmptyProps = {
    /** Icon shown above the title. */
    icon?: Snippet;
    /** Heading of the empty state. */
    title?: string;
    /** Text under the heading. */
    description?: string;
    /** Action shown under the text. */
    action?: Snippet;
    /** Content rendered inside. */
    children?: Snippet;
} & DefaultProps &
    Omit<HTMLAttributes<HTMLDivElement>, 'children'>;

export type ConversationScrollButtonProps = {
    /** Classes added to the element. */
    class?: string;
} & Omit<
    HTMLButtonAttributes,
    'children' | 'class' | 'aria-label' | 'disabled' | 'tabindex' | 'type'
>;

export { Content, Empty, Root, ScrollButton };
~~~~

## Examples

### Empty State

~~~~svelte
<script lang="ts">
    import { File01Icon as FileText } from '@hugeicons/core-free-icons';
    import { Button } from '@mielui/svelte/components/button';
    import * as Conversation from '@mielui/svelte/components/conversation';
    import * as Message from '@mielui/svelte/components/message';
    import HugeiconsIcon from '@mielui/svelte/hugeicons-icon';

    let started = $state(false);

    function start() {
        started = true;
    }
</script>

{#snippet emptyIcon()}
    <HugeiconsIcon icon={FileText} size={18} strokeWidth={1.75} aria-hidden="true" />
{/snippet}

{#snippet startAction()}
    <Button size="md" onclick={start}>Draft a release plan</Button>
{/snippet}

<Conversation.Root class="h-[22rem] w-full max-w-2xl mielui-plate">
    <Conversation.Content aria-label="Release planning conversation">
        {#if started}
            <Message.Root from="user">
                <Message.Content>
                    Draft a release plan for the checkout reliability fix.
                </Message.Content>
            </Message.Root>
            <Message.Root from="assistant">
                <Message.Content>
                    I’ll start with the rollout stages, owners, health checks, and rollback
                    threshold.
                </Message.Content>
            </Message.Root>
        {:else}
            <Conversation.Empty
                icon={emptyIcon}
                title="Plan the next release"
                description="Turn an issue or change set into a staged rollout with clear checks."
                action={startAction}
            />
        {/if}
    </Conversation.Content>
</Conversation.Root>
~~~~

### Follow Output

~~~~svelte
<script lang="ts">
    import { Add01Icon as Plus } from '@hugeicons/core-free-icons';
    import { Button } from '@mielui/svelte/components/button';
    import * as Conversation from '@mielui/svelte/components/conversation';
    import * as Message from '@mielui/svelte/components/message';
    import HugeiconsIcon from '@mielui/svelte/hugeicons-icon';

    let follow = $state(true);
    let output = $state([
        { id: 1, label: 'Queued release web-2419' },
        { id: 2, label: 'Built application bundle in 18.2s' },
        { id: 3, label: 'Uploaded 42 immutable assets' },
        { id: 4, label: 'Deployed canary to iad1' },
        { id: 5, label: 'Passed checkout smoke tests' },
        { id: 6, label: 'Shifted 10% of production traffic' },
        { id: 7, label: 'Observed p95 latency at 612 ms' }
    ]);

    const updates = [
        'Shifted 25% of production traffic',
        'Observed p95 latency at 584 ms',
        'Passed payment-provider health check',
        'Shifted 50% of production traffic'
    ];
    const followLabel = $derived(follow ? 'Following latest output' : 'Follow paused');

    function appendOutput() {
        const index = output.length - 7;
        output.push({
            id: output.length + 1,
            label: updates[index % updates.length]
        });
    }
</script>

<div class="w-full max-w-2xl space-y-3">
    <div class="flex flex-wrap items-center justify-between gap-3">
        <p class="flex items-center gap-2 text-sm text-foreground-muted" role="status">
            <span class="size-1.5 rounded-full bg-primary" aria-hidden="true"></span>
            {followLabel}
        </p>
        <Button variant="ghost" size="md" onclick={appendOutput}>
            <HugeiconsIcon icon={Plus} size={14} aria-hidden="true" />
            Append output
        </Button>
    </div>

    <Conversation.Root bind:follow class="h-[22rem] mielui-plate">
        <Conversation.Content aria-label="Live deployment output">
            {#each output as entry (entry.id)}
                <Message.Root from="assistant">
                    <Message.Content>{entry.label}</Message.Content>
                </Message.Root>
            {/each}
        </Conversation.Content>
        <Conversation.ScrollButton />
    </Conversation.Root>

    <p class="text-xs leading-5 text-foreground-muted">
        Scroll up to pause following. Append more output, then use the arrow to return to the latest
        step.
    </p>
</div>
~~~~

### Hero

~~~~svelte
<script lang="ts">
    import * as Conversation from '@mielui/svelte/components/conversation';
    import { Markdown } from '@mielui/svelte/components/markdown';
    import * as Message from '@mielui/svelte/components/message';
    import * as Reasoning from '@mielui/svelte/components/reasoning';

    const finding = [
        '### What changed',
        '',
        'The latency increase starts in `POST /checkout` immediately after release `web-2418`.',
        '',
        '- Address validation added **430 ms** at p95.',
        '- The provider timed out for 8% of non-US requests.',
        '- Database and inventory spans stayed within baseline.',
        '',
        '> Roll back the synchronous validation call, then keep the rule behind the existing review queue.'
    ].join('\n');

    const rollback = [
        'Use the targeted flag first:',
        '',
        '```ts',
        "await flags.disable('checkout.address-verification');",
        'await metrics.waitForRecovery({ window: "10m" });',
        '```',
        '',
        'This avoids reverting the unrelated tax fixes in the same release.'
    ].join('\n');
</script>

<Conversation.Root class="h-[30rem] w-full max-w-3xl mielui-plate">
    <Conversation.Content aria-label="Checkout incident conversation">
        <Message.Root from="user">
            <Message.Content>
                Investigate why checkout latency rose after 14:00 UTC. Focus on the latest release
                and give me a safe mitigation.
            </Message.Content>
        </Message.Root>

        <Message.Root from="assistant">
            <Message.Content class="space-y-4">
                <Reasoning.Root open>
                    <Reasoning.Trigger
                        title="Compared traces with the release timeline"
                        duration="4.8s"
                    />
                    <Reasoning.Content>
                        <p>
                            Separated provider, database, and inventory spans, then matched the
                            first regression to the release boundary.
                        </p>
                    </Reasoning.Content>
                </Reasoning.Root>
                <Markdown content={finding} />
            </Message.Content>
        </Message.Root>

        <Message.Root from="user">
            <Message.Content>Show me the smallest rollback and how to verify it.</Message.Content>
        </Message.Root>

        <Message.Root from="assistant">
            <Message.Content><Markdown content={rollback} /></Message.Content>
        </Message.Root>
    </Conversation.Content>
    <Conversation.ScrollButton />
</Conversation.Root>
~~~~

For the rendered reference, visit [/docs/components/conversation](/docs/components/conversation).
