ChatTool Soon

Display a collapsible AI tool invocation status.

Usage

The ChatTool component renders a collapsible block that displays AI tool invocation status, such as "Searching components" or "Reading documentation". When a default slot is provided, it becomes collapsible to reveal tool output.

<script setup lang="ts">
const streaming = ref(true)
const result = ref(`$ pnpm run lint

> eslint .

✔ No lint errors found.
`)

let timer: ReturnType<typeof setTimeout> | undefined

onMounted(() => {
  timer = setTimeout(() => {
    streaming.value = false
  }, 5000)
})

onUnmounted(() => {
  clearTimeout(timer)
})
</script>

<template>
  <UChatTool
    :text="streaming ? 'Running lint checks' : 'Lint checks completed'"
    suffix="cd, pnpm run"
    :streaming="streaming"
    icon="i-lucide-terminal"
    variant="card"
    chevron="leading"
    class="w-80"
  >
    <pre language="bash" v-text="result" />
  </UChatTool>
</template>

Text

Use the text prop to set the tool status text.

<template>
  <UChatTool text="Searched components" />
</template>

Suffix

Use the suffix prop to display secondary text after the main label.

<template>
  <UChatTool text="Reading component" suffix="Button" />
</template>

Streaming

Use the streaming prop to indicate the tool is actively running. The text displays a shimmer animation.

<template>
  <UChatTool streaming text="Searching components..." />
</template>
Use the isToolStreaming utility from @nuxt/ui/utils/ai to determine if a tool part is still running.

Shimmer

When streaming, the trigger label uses the ChatShimmer component. Use the shimmer prop to customize its duration and spread.

<template>
  <UChatTool
    streaming
    text="Searching components..."
    :shimmer="{
      duration: 2,
      spread: 2
    }"
  />
</template>

Icon

Use the icon prop to display an Icon component next to the trigger.

<template>
  <UChatTool icon="i-lucide-search" text="Searched components" />
</template>

Loading

Use the loading prop to show a loading indicator. Use the loading-icon prop to customize the loading icon.

<template>
  <UChatTool loading text="Searching components..." />
</template>

Loading Icon

Use the loading-icon prop to customize the loading icon. Defaults to i-lucide-loader-circle.

<template>
  <UChatTool loading loading-icon="i-lucide-loader" text="Searching components..." />
</template>
You can customize this icon globally in your app.config.ts under ui.icons.loading key.
You can customize this icon globally in your vite.config.ts under ui.icons.loading key.

Chevron

Use the chevron prop to change the position of the chevron icon.

When chevron is set to leading with an icon, the icon swaps with the chevron on hover and when open.
<template>
  <UChatTool chevron="leading" icon="i-lucide-search" text="Searched components">
    Tool output content
  </UChatTool>
</template>

Chevron Icon

Use the chevron-icon prop to customize the chevron Icon. Defaults to i-lucide-chevron-down.

<template>
  <UChatTool chevron-icon="i-lucide-arrow-down" text="Searched components">
    Tool output content
  </UChatTool>
</template>
You can customize this icon globally in your app.config.ts under ui.icons.chevronDown key.
You can customize this icon globally in your vite.config.ts under ui.icons.chevronDown key.

Variant

Use the variant prop to change the visual style. Defaults to inline.

<template>
  <UChatTool variant="card" text="Searched components" icon="i-lucide-search" chevron="trailing">
    Tool output content
  </UChatTool>
</template>

Examples

Check the Chat overview page for installation instructions, server setup and usage examples.

API

Props

Prop Default Type
text string

The text content to display.

suffix string

The suffix text displayed after the main text.

iconany

The icon displayed next to the trigger.

loadingfalseboolean

Whether the tool is in a loading state.

loadingIconappConfig.ui.icons.loadingany

The icon displayed when loading.

streamingfalseboolean

Whether the tool content is currently streaming.

variant'inline' "card" | "inline"

The visual variant of the tool display.

chevron'trailing' "leading" | "trailing"

The position of the chevron icon.

chevronIconappConfig.ui.icons.chevronDownany

The icon displayed as the chevron.

shimmer Partial<Omit<ChatShimmerProps, "text">>

Customize the ChatShimmer component when streaming.

disabledboolean

When true, prevents the user from interacting with the collapsible.

openundefinedboolean

The controlled open state of the collapsible. Can be binded with v-model.

defaultOpenboolean

The open state of the collapsible when it is initially rendered.
Use when you do not need to control its open state.

unmountOnHidefalseboolean

When true, the element will be unmounted on closed state.

ui { root?: ClassNameValue; trigger?: ClassNameValue; leading?: ClassNameValue; leadingIcon?: ClassNameValue; chevronIcon?: ClassNameValue; label?: ClassNameValue; suffix?: ClassNameValue; trailingIcon?: ClassNameValue; content?: ClassNameValue; body?: ClassNameValue; }

Slots

Slot Type
default{ open: boolean; }

Emits

Event Type
update:open[value: boolean]

Theme

app.config.ts
export default defineAppConfig({
  ui: {
    chatTool: {
      slots: {
        root: '',
        trigger: [
          'group flex w-full items-center gap-1.5 text-muted text-sm disabled:cursor-default disabled:hover:text-muted hover:text-default focus-visible:outline-offset-2 focus-visible:outline-primary min-w-0',
          'transition-colors'
        ],
        leading: 'relative size-4 shrink-0',
        leadingIcon: 'size-4 shrink-0',
        chevronIcon: 'size-4 shrink-0 group-data-[state=open]:rotate-180 transition-transform duration-200',
        label: 'truncate',
        suffix: 'text-dimmed ms-1',
        trailingIcon: 'size-4 shrink-0 group-data-[state=open]:rotate-180 transition-transform duration-200',
        content: 'data-[state=open]:animate-[collapsible-down_200ms_ease-out] data-[state=closed]:animate-[collapsible-up_200ms_ease-out] overflow-hidden',
        body: 'text-sm text-dimmed whitespace-pre-wrap'
      },
      variants: {
        variant: {
          inline: {
            body: 'pt-2'
          },
          card: {
            root: 'rounded-md ring ring-default overflow-hidden',
            trigger: 'px-2 py-1',
            trailingIcon: 'ms-auto',
            body: 'border-t border-default p-2 max-h-[200px] overflow-y-auto'
          }
        },
        chevron: {
          leading: {
            leadingIcon: 'group-hover:opacity-0'
          },
          trailing: ''
        },
        loading: {
          true: {
            leadingIcon: 'animate-spin'
          }
        },
        alone: {
          false: {
            leadingIcon: [
              'absolute inset-0 group-data-[state=open]:opacity-0',
              'transition-opacity duration-200'
            ],
            chevronIcon: [
              'absolute inset-0 opacity-0 group-hover:opacity-100 group-data-[state=open]:opacity-100',
              'transition-[rotate,opacity] duration-200'
            ]
          }
        }
      },
      defaultVariants: {
        variant: 'inline'
      }
    }
  }
})
vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'

export default defineConfig({
  plugins: [
    vue(),
    ui({
      ui: {
        chatTool: {
          slots: {
            root: '',
            trigger: [
              'group flex w-full items-center gap-1.5 text-muted text-sm disabled:cursor-default disabled:hover:text-muted hover:text-default focus-visible:outline-offset-2 focus-visible:outline-primary min-w-0',
              'transition-colors'
            ],
            leading: 'relative size-4 shrink-0',
            leadingIcon: 'size-4 shrink-0',
            chevronIcon: 'size-4 shrink-0 group-data-[state=open]:rotate-180 transition-transform duration-200',
            label: 'truncate',
            suffix: 'text-dimmed ms-1',
            trailingIcon: 'size-4 shrink-0 group-data-[state=open]:rotate-180 transition-transform duration-200',
            content: 'data-[state=open]:animate-[collapsible-down_200ms_ease-out] data-[state=closed]:animate-[collapsible-up_200ms_ease-out] overflow-hidden',
            body: 'text-sm text-dimmed whitespace-pre-wrap'
          },
          variants: {
            variant: {
              inline: {
                body: 'pt-2'
              },
              card: {
                root: 'rounded-md ring ring-default overflow-hidden',
                trigger: 'px-2 py-1',
                trailingIcon: 'ms-auto',
                body: 'border-t border-default p-2 max-h-[200px] overflow-y-auto'
              }
            },
            chevron: {
              leading: {
                leadingIcon: 'group-hover:opacity-0'
              },
              trailing: ''
            },
            loading: {
              true: {
                leadingIcon: 'animate-spin'
              }
            },
            alone: {
              false: {
                leadingIcon: [
                  'absolute inset-0 group-data-[state=open]:opacity-0',
                  'transition-opacity duration-200'
                ],
                chevronIcon: [
                  'absolute inset-0 opacity-0 group-hover:opacity-100 group-data-[state=open]:opacity-100',
                  'transition-[rotate,opacity] duration-200'
                ]
              }
            }
          },
          defaultVariants: {
            variant: 'inline'
          }
        }
      }
    })
  ]
})

Changelog

No recent changes