---
title: 'Chat components'
description: 'Building blocks for human and AI conversations, including messages, conversation scrolling, markdown responses and message input.'
version: 11.17.1
generatedAt: 2026-11-10T20:21:10.551Z
checksum: c98549d6d82cf566cf07dbaaafd9f4d083b7f9e9d4dd8a59e9aabbb92363c7db
---

# Chat components

**Experimental:** The Chat extension and its component APIs may change in the future. Please give us feedback on your experience with them.

## Import

```tsx
import {
  Conversation,
  Message,
  PromptInput,
  Response,
} from '@dnb/eufemia/extensions/chat'
import '@dnb/eufemia/extensions/chat/style'
```

Import the components and hooks you use by name. The `Chat.` prefix in the documentation identifies the extension; the examples use named imports directly.

### Isolated styles

For applications using [style isolation](/web/usage/customisation/styling/style-isolation/), replace the regular Chat style import with the isolated entry point:

```tsx
import '@dnb/eufemia/style/isolated'
import '@dnb/eufemia/extensions/chat/style/isolated'
import { IsolatedStyleScope } from '@dnb/eufemia/shared'
import { Response } from '@dnb/eufemia/extensions/chat'

render(
  <IsolatedStyleScope>
    <Response>{'Hello from the assistant.'}</Response>
  </IsolatedStyleScope>
)
```

The Chat entry point adds only the extension styles. Keep the isolated basis and theme imports for the Eufemia components used inside it.

## Description

`Chat` provides building blocks for conversations between people, with an AI assistant, or with a handover between the two. Compose messages, a conversation history and an input using Eufemia components. No AI service or chat framework is required.

Use plain text or your own content in `Chat.Message`. Add `Chat.Response` when messages contain markdown. Components such as `Task`, `Confirmation`, `AgentAction` and `Sources` are optional and can represent application operations, decisions and references.

The application owns the participants and their identities. The existing `from="user"` and `from="assistant"` values identify the two conversation sides: `user` is the current user on the right, and `assistant` is the other side on the left, including a human support adviser. They do not determine whether content is AI-generated. Use `name` and `avatar` to identify the sender, and set `aiGenerated` only for content that is actually generated by AI.

## Compatibility with Ai

`Chat` is the new name for the extension previously called `Ai`. Existing `Ai` imports, component types, locale imports and regular or isolated styles remain available as compatibility aliases. Both names use the same components and conversation state. Prefer `Chat` and `@dnb/eufemia/extensions/chat` in new code.

```tsx
import { Chat } from '@dnb/eufemia/extensions'
import '@dnb/eufemia/extensions/chat/style'

render(
  <Chat.Message name="Customer support">How can I help?</Chat.Message>
)
```

Existing `.dnb-ai-*` CSS classes and `--ai-*` custom properties are retained so application styling keeps working. Translation overrides should use `Chat`; legacy `Ai` overrides remain supported, with `Chat` taking precedence when both provide the same key. See [Translations](/web/patterns-and-extensions/chat/translations/) and the [v13 migration note](/web/releases-and-upgrades/versions/eufemia/v13-info/#chat-extension).

## Usage

Your application owns the messages, delivery, participant identities, operation execution and error handling. Eufemia renders the content and handles presentation, keyboard interaction and conversation scrolling.

The message type below belongs to this example. Use your application's data and map it to the component props.

```tsx
import { useId } from 'react'
import {
  Conversation,
  Message,
  MessageIndicator,
  PromptInput,
  Response,
} from '@dnb/eufemia/extensions/chat'
import { GlobalStatus } from '@dnb/eufemia/components'
import '@dnb/eufemia/extensions/chat/style'

type ChatMessage = {
  id: string
  from: 'user' | 'assistant'
  text: string
  isStreaming?: boolean
}

type ChatProps = {
  messages: Array<ChatMessage>
  isBusy: boolean
  isWaiting: boolean
  hasError: boolean
  onSend: (text: string) => void
  onStop: () => void
}

export function Chat({
  messages,
  isBusy,
  isWaiting,
  hasError,
  onSend,
  onStop,
}: ChatProps) {
  const prefix = useId()
  return (
    <>
      <Conversation
        label="Customer support chat"
        scrollBehavior="end"
        style={{ height: '32rem' }}
      >
        {messages.map((message) => (
          <Message
            key={message.id}
            id={prefix + message.id}
            from={message.from}
          >
            {message.from === 'user' ? (
              message.text
            ) : (
              <Response
                parseIncompleteMarkdown={message.isStreaming ?? false}
              >
                {message.text}
              </Response>
            )}
          </Message>
        ))}
        {isWaiting && <MessageIndicator />}
      </Conversation>
      {hasError && (
        <GlobalStatus
          show
          autoScroll={false}
          omitSetFocus
          id={prefix + '-error'}
          text="The reply could not be completed. Try again."
        />
      )}
      <PromptInput
        isBusy={isBusy}
        onSubmit={({ value }) => onSend(value)}
        onStop={onStop}
      />
    </>
  )
}
```

Compose tool results and sources inside each message using `Chat.Task` and `Chat.Sources`. See [Integrations](/web/patterns-and-extensions/chat/integrations/) for complete framework examples and ownership of each state.

Use [Chat.AgentAction](/web/patterns-and-extensions/chat/AgentAction/) for compact agent actions with progress, a result, and optional Undo. The application controls the action state and reversal window.

Use [Chat.Confirmation](/web/patterns-and-extensions/chat/Confirmation/) to ask the user to confirm or decline before proceeding with a proposed action.

## Guides

For human conversations, use `scrollBehavior="end"` to follow the latest messages. Show a MessageIndicator only when your application has a typing signal, and give it a suitable label. See the [human support chat example](/web/patterns-and-extensions/chat/demos/#human-support-chat).

AI integrations can stream markdown and expose tool operations. For streamed-reply announcements, follow the [application-owned announcement recipe](/web/patterns-and-extensions/chat/integrations/#screen-reader-announcements).

- [Translations](/web/patterns-and-extensions/chat/translations/): select a locale and customize component labels.
- [Integrations](/web/patterns-and-extensions/chat/integrations/): composition and recipes for AI SDK and TanStack AI.

- [Message scroller](/web/patterns-and-extensions/chat/message-scroller/): how the conversation scrolls while replies stream in.

## Components


## [Chat.Actions](/web/patterns-and-extensions/chat/Actions/)

Shows buttons for a message, like copy or feedback.

## [Chat.ActivityText](/web/patterns-and-extensions/chat/ActivityText/)

Describes an ongoing activity in the conversation, such as searching or preparing a reply.

## [Chat.AgentAction](/web/patterns-and-extensions/chat/AgentAction/)

Shows an agent action progressing, its result, and an optional Undo.

## [Chat.Confirmation](/web/patterns-and-extensions/chat/Confirmation/)

Asks the user to confirm or decline a proposed action.

## [Chat.Conversation](/web/patterns-and-extensions/chat/Conversation/)

Holds the messages of a chat and keeps the latest message in view while the reply streams in.

## [Chat.DateMarker](/web/patterns-and-extensions/chat/DateMarker/)

Shows the date above the messages of that day.

## [Chat.Info](/web/patterns-and-extensions/chat/Info/)

Shows supporting information about a conversation.

## [Chat.Message](/web/patterns-and-extensions/chat/Message/)

Shows a message from either side of a human or AI conversation, as a bubble or as plain content.

## [Chat.MessageIndicator](/web/patterns-and-extensions/chat/MessageIndicator/)

Indicates that a reply is being prepared or another participant is typing.

## [Chat.PromptInput](/web/patterns-and-extensions/chat/PromptInput/)

Where the user writes a message, with a send button that becomes a stop button while the reply streams in.

## [Chat.Response](/web/patterns-and-extensions/chat/Response/)

Renders complete or streaming markdown messages with Eufemia elements.

## [Chat.Sources](/web/patterns-and-extensions/chat/Sources/)

Shows a collapsible list of the sources used in a reply.

## [Chat.Suggestions](/web/patterns-and-extensions/chat/Suggestions/)

Shows prompts the user can choose, as chips or cards.

## [Chat.Task](/web/patterns-and-extensions/chat/Task/)

Shows work in progress and its outcome, with optional details or results.

## [Chat.Welcome](/web/patterns-and-extensions/chat/Welcome/)

Greets the user before the conversation starts.


## Relevant links

- [Source code](https://github.com/dnbexperience/eufemia/tree/main/packages/dnb-eufemia/src/extensions/chat)
- [Docs code](https://github.com/dnbexperience/eufemia/tree/main/packages/dnb-design-system-portal/src/docs/web/patterns-and-extensions/chat)


## Examples

### Human support chat

Messages can come from people without an AI service. Use `from="user"` for the current user and `from="assistant"` for the support adviser. Provide their names and avatars, and omit `aiGenerated`. `scrollBehavior="end"` follows the latest messages while preserving the reader's position when they scroll back. The application owns delivery and any typing signal.


```tsx
const SupportChat = () => {
  const [messages, setMessages] = useState<Array<{
    id: string;
    from: 'user' | 'assistant';
    text: string;
  }>>([{
    id: 'welcome',
    from: 'assistant' as const,
    text: 'Hi! My name is Sara. How can I help you today?'
  }]);
  return <Flex.Stack>
              <Conversation label="Chat with customer support" scrollBehavior="end" style={{
      height: '16rem'
    }}>
                {messages.map(message => <Message key={message.id} from={message.from} name={message.from === 'user' ? 'You' : 'Sara, customer support'} avatar={message.from === 'assistant' ? <Avatar icon={customer_service} hasLabel /> : undefined}>
                    {message.text}
                  </Message>)}
              </Conversation>
              <PromptInput label="Message to customer support" placeholder="Write a message to Sara" onSubmit={({
      value
    }) => setMessages(current => [...current, {
      id: String(current.length),
      from: 'user',
      text: value
    }])} />
              <P size="small">
                This local demo adds your messages. A support adviser is
                not connected.
              </P>
            </Flex.Stack>;
};
render(<SupportChat />);
```


### Full chat experience

A complete chat with an AI assistant. Choose a suggestion or write a message, e.g. "Block my card" or "Compare my spending", to see the tools, answer and sources stream in. Press stop to end the reply, or regenerate the last answer.

The demo uses scripted replies and local chat state. Connect your own messages, status, send, stop and regenerate actions to the components in your app.

New chat opens an empty conversation and clears any draft or pending reply. The demo content and component labels are in English. See [Translations](/web/patterns-and-extensions/chat/translations/) for changing component labels; your application supplies translated chat content.


```tsx
const primarySuggestions = [{
  icon: card,
  prompt: 'Block my Visa card'
}, {
  icon: bank,
  prompt: 'Compare my spending with last month'
}, {
  icon: transfer,
  prompt: 'Show my latest transactions'
}];
const suggestionChips = [{
  icon: loan,
  label: 'loan offers',
  prompt: 'What loan offers do you have?'
}, {
  label: 'what can you do?',
  prompt: 'What can you help me with?'
}];
const ainoAvatar = <Avatar size="small" icon={aino} hasLabel />;
const BankingAssistant = () => {
  const [chatStarted, setChatStarted] = useState(false);
  const [draft, setDraft] = useState('');
  const promptRef = useRef<HTMLTextAreaElement>(null);
  const [feedback, setFeedback] = useState({});
  const {
    messages,
    status,
    sendMessage,
    stop,
    regenerate,
    setMessages
  } = useChatSimulation();
  const send = text => sendMessage({
    text
  });
  const questions = messages.filter(({
    role
  }) => role === 'user');
  return <Provider locale="en-GB">
              <ChatLayout>
                <section className="chat" style={chatStyle}>
                  <Flex.Horizontal align="center" justify="space-between">
                    <Flex.Horizontal align="center" gap="small">
                      {ainoAvatar}
                      <div>
                        <strong>Aino</strong>
                        <P size="small">DNB Banking Assistant</P>
                      </div>
                    </Flex.Horizontal>

                    <Button variant="tertiary" icon={add} iconPosition="left" onClick={() => {
            setMessages([]);
            setFeedback({});
            setDraft('');
            setChatStarted(true);
            promptRef.current?.focus();
          }}>
                      New chat
                    </Button>
                  </Flex.Horizontal>

                  {!chatStarted && messages.length === 0 ? <Welcome greeting="Hello, Peter" avatar={<Avatar size="small" icon={aino} hasLabel />} title="Where do you want to start today?" style={{
          margin: 'auto 0'
        }}>
                      <Suggestions>
                        {primarySuggestions.map(({
              icon,
              prompt
            }) => <Suggestion key={prompt} icon={icon} suggestion={prompt} onClick={({
              suggestion
            }) => {
              send(suggestion);
              promptRef.current?.focus();
            }} />)}
                      </Suggestions>
                      <Suggestions>
                        {suggestionChips.map(({
              icon,
              label,
              prompt
            }) => <Suggestion key={label} icon={icon} suggestion={prompt} onClick={({
              suggestion
            }) => {
              send(suggestion);
              promptRef.current?.focus();
            }}>
                            {label}
                          </Suggestion>)}
                      </Suggestions>
                    </Welcome> : <Conversation id="banking-chat" aria-live="off" scrollBehavior="end" style={{
          flex: '1 1 auto'
        }}>
                      <DateMarker date={new Date()} />
                      <Info>
                        <P>
                          Aino is a chatbot for customer service. Do not
                          share personal information with the chatbot. Read
                          more about{' '}
                          <Anchor href="https://www.dnb.no/personvern">
                            your privacy
                          </Anchor>
                          .
                        </P>
                        <P top>
                          Some answers are generated with artificial
                          intelligence and are marked with a tag.
                          AI-generated answers can contain errors.
                        </P>
                      </Info>

                      {messages.map((message, index) => {
            const isAssistant = message.role === 'assistant';
            const isLast = index === messages.length - 1;
            const isDone = !(isLast && status === 'streaming');
            const transactions = getTransactions(message);
            const actions = isAssistant && isDone && <Actions>
                            <CopyOnClick.Button size="small" iconSize="default" bounding copyContent={getText(message)} title="Copy response" tooltip="Copy response" />
                            <Action icon={thumbs_up} label="Good answer" selected={feedback[message.id] === true} onClick={() => setFeedback(all => ({
                ...all,
                [message.id]: all[message.id] === true ? undefined : true
              }))} />
                            <Action icon={thumbs_down} label="Bad answer" selected={feedback[message.id] === false} onClick={() => setFeedback(all => ({
                ...all,
                [message.id]: all[message.id] === false ? undefined : false
              }))} />
                            {isLast && <Action icon={refresh} label="Regenerate" onClick={() => {
                regenerate();
                promptRef.current?.focus();
              }} />}
                          </Actions>;
            return <Fragment key={message.id}>
                            <Message id={message.id} from={message.role} aria-label={isAssistant ? 'Aino' : 'You'} grouped={isAssistant && messages[index - 1]?.role === 'assistant'} avatar={isAssistant ? ainoAvatar : undefined} aiGenerated={isAssistant} actions={transactions ? undefined : actions}>
                              <DemoMessageContent message={message} />
                              {message.stopped && <P size="small">Reply stopped.</P>}
                            </Message>

                            {transactions && <Message variant="plain" actions={actions}>
                                <List.Container>
                                  {transactions.map(({
                    name,
                    account,
                    date,
                    amount
                  }) => <List.Item.Action key={name} icon={amount < 0 ? transfer : pay_from} onClick={() => send(`Show the payment confirmation for ${name}`)}>
                                        <List.Cell.Title>
                                          <List.Cell.Title.Overline>
                                            {account} · {date}
                                          </List.Cell.Title.Overline>
                                          {name}
                                        </List.Cell.Title>
                                        <List.Cell.End>
                                          <NumberFormat.Currency value={amount} />
                                        </List.Cell.End>
                                      </List.Item.Action>)}
                                </List.Container>
                              </Message>}
                          </Fragment>;
          })}

                      {status === 'submitted' && <MessageIndicator name="Aino" avatar={ainoAvatar} />}
                    </Conversation>}
                  <ReplyAnnouncement isBusy={status !== 'ready'} messageId={messages.at(-1)?.role === 'assistant' ? messages.at(-1).id : undefined} outcome={messages.at(-1)?.stopped ? 'Reply stopped.' : undefined} />

                  <PromptInput value={draft} textareaProps={{
          ref: promptRef
        }} onChange={({
          value
        }) => setDraft(value)} isBusy={status === 'submitted' || status === 'streaming'} characterCounter={200} onSubmit={({
          value
        }) => {
          send(value);
          setDraft('');
        }} onStop={stop} />
                </section>

                {questions.length > 1 && <QuestionOverview questions={questions} />}
              </ChatLayout>
            </Provider>;
};

// Jump between the questions, and see which one you read
// Jump between the questions, and see which one you read
const QuestionOverview = ({
  questions
}) => {
  const {
    scrollToMessage
  } = useConversation('banking-chat');
  const {
    currentTurnId
  } = useConversationVisibility('banking-chat');
  return <nav aria-label="Questions in this chat">
              <P bottom="x-small">
                <strong>Questions</strong>
              </P>
              <List.Container>
                {questions.map(question => <List.Item.Action key={question.id} selected={question.id === currentTurnId} aria-current={question.id === currentTurnId ? 'step' : undefined} onClick={() => scrollToMessage(question.id)}>
                    <List.Cell.Title>{getText(question)}</List.Cell.Title>
                  </List.Item.Action>)}
              </List.Container>
            </nav>;
};
render(<BankingAssistant />);
```


### Chat window and customer-service handover

Compose a chat window with Dialog for its keyboard focus, close controls and responsive modal behavior. The input uses the large radius token inside the window. Suggestions sit above the input; the application owns sending them. A customer-service reply uses the assistant side with a headset avatar and no AI-generated label.


```tsx
render(<Dialog restoreFocusTo={() => document.getElementById('ai-chat-window-trigger')} trigger={props => <Button {...props} id="ai-chat-window-trigger">
            Open chat window
          </Button>} minWidth="23rem" maxWidth="23rem" title="Chat with DNB">
        <Flex.Stack gap="small" style={{
    height: 'min(38rem, 65dvh)',
    minHeight: '18rem'
  }}>
          <Conversation scrollBehavior="end" aria-live="off" style={{
      flex: '1 1 auto'
    }}>
            <Message from="user">Can I speak to a person?</Message>
            <Message aria-label="Aino" avatar={<Avatar size="small" icon={aino} hasLabel />}>
              I will connect you to customer service.
            </Message>
            <Message aria-label="Maria, customer service" avatar={<Avatar size="small" icon={customer_service} hasLabel />}>
              Hello, I am Maria from DNB. How can I help?
            </Message>
          </Conversation>
          <Suggestions aria-label="Suggested replies">
            <Suggestion suggestion="Help with a payment" />
            <Suggestion suggestion="Help with my card" />
          </Suggestions>
          <PromptInput style={{
      '--ai-prompt-input-radius': 'var(--token-radius-lg)'
    } as React.CSSProperties} />
        </Flex.Stack>
      </Dialog>)
```


### Streamed reply announcements

The application keeps a separate `AriaLive` region mounted and updates it once when the reply ends, including when Stop preserves a partial answer. The conversation's automatic live announcements are disabled to avoid duplicates. See [Screen reader announcements](/web/patterns-and-extensions/chat/integrations/#screen-reader-announcements) for the application helper and failure handling.


```tsx
const ChatDemo = () => {
  const {
    messages,
    status,
    sendMessage,
    stop
  } = useChatSimulation();
  const isBusy = status !== 'ready';
  const reply = messages.at(-1);
  return <>
              <Conversation aria-live="off" style={{
      height: '18rem'
    }}>
                {messages.map(message => <Message key={message.id} id={message.id} from={message.role}>
                    <DemoMessageContent message={message} />
                    {message.stopped && <P size="small">Reply stopped.</P>}
                  </Message>)}
              </Conversation>
              <ReplyAnnouncement isBusy={isBusy} messageId={reply?.role === 'assistant' ? reply.id : undefined} outcome={reply?.stopped ? 'Reply stopped.' : undefined} />
              <PromptInput isBusy={isBusy} onSubmit={({
      value
    }) => {
      sendMessage({
        text: value
      });
    }} onStop={() => {
      stop();
    }} />
            </>;
};
render(<ChatDemo />);
```


### Chat with compact input


```tsx
render(<Welcome greeting="Hello, Peter" avatar={<Avatar size="small" icon={aino} hasLabel />} title="Where do you want to start today?">
        <Suggestions>
          <Suggestion icon={bank} suggestion="Explain my spending this month" />
          <Suggestion icon={bank} suggestion="Show my savings goals" />
          <Suggestion icon={bank} suggestion="When is my next bill due?" />
        </Suggestions>
        <PromptInput variant="compact" placeholder="Start typing here" />
      </Welcome>)
```


### Chat with content in the conversation

User messages are placed on the right. Content like a list can be shown without a bubble by using `variant="plain"`.


```tsx
render(<div style={chatStyle}>
        <Conversation style={{
    flex: '1 1 auto'
  }}>
          <Message from="user" name="You" timestamp="15:23" avatar={<Avatar hasLabel>P</Avatar>}>
            Hi, I need the payment confirmation for a transfer I made from
            my salary account 12.02
          </Message>

          <Message name="Aino" timestamp="15:24" avatar={<Avatar variant="secondary" hasLabel>
                A
              </Avatar>}>
            Hi, it seems you made 2 transfers 12.02. I have provided both
            for you below. Just click the correct one and you will be able
            to get to the payment confirmation.
          </Message>

          <Message variant="plain">
            <List.Container>
              <List.Item.Action icon={transfer}>
                <List.Cell.Title>
                  <List.Cell.Title.Overline>
                    (xxxx.xx.xxxxx)
                  </List.Cell.Title.Overline>
                  Intro Aksel
                </List.Cell.Title>
                <List.Cell.End>
                  <NumberFormat.Currency value={-888888} />
                </List.Cell.End>
              </List.Item.Action>
              <List.Item.Action icon={pay_from}>
                <List.Cell.Title>
                  <List.Cell.Title.Overline>
                    (xxxx.xx.xxxxx)
                  </List.Cell.Title.Overline>
                  Kim Olsen
                </List.Cell.Title>
                <List.Cell.End>
                  <NumberFormat.Currency value={-888888} />
                </List.Cell.End>
              </List.Item.Action>
            </List.Container>
          </Message>
        </Conversation>

        <PromptInput characterCounter={111} />
      </div>)
```
