---
title: 'Ai.PromptInput'
description: 'Where the user writes a message, with a send button that becomes a stop button while the reply streams in.'
version: 11.17.0
generatedAt: 2026-11-05T09:55:47.092Z
checksum: 384e27d9a907638a8616fd217116d6c57c0dca952227b4cf06fcb2cc0bf9fdd9
---

# Ai.PromptInput

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

## Import

```tsx
import { PromptInput } from '@dnb/eufemia/extensions/ai'
import '@dnb/eufemia/extensions/ai/style'
render(<PromptInput />)
```

## Description

`Ai.PromptInput` is where the user writes a message. On desktop, Enter sends the message and Shift+Enter adds a new line. On touch-primary devices (`hover: none` and `pointer: coarse`), Enter adds a new line; use the send button to submit. Set `isBusy` while a reply is pending or streaming. With `onStop`, the send button becomes a stop button. Pressing empty space on the card focuses the textarea with the caret at the end. Custom controls and context content keep their own interactions. After sending, focus returns to the textarea. Stopping also returns keyboard focus when the empty compact input removes the stop button.

Use `variant="compact"` for a single line on a welcome screen. It shows the Aino icon by default and hides the send button until the user enters text. While busy, the stop button stays available even when the input is empty. Use `icon` to change the leading icon or `icon={null}` to hide it.

The default variant is flat and gains a shadow when the textarea is focused. The card uses Textarea hover and focus border colors and thickness; its toolbar buttons retain their own focus styles. The compact variant has a shadow by default; set `dropShadow={false}` for an inline surface or `dropShadow` to elevate the default variant. Keyboard focus remains visible in both variants.

With `disabled`, the card uses Textarea's disabled background and border tokens. Set `textareaProps.status` for error feedback; the card keeps its error border while the textarea is focused.

Pass context chips as `children` to show attachments or references above the input. The context area opens and closes with HeightAnimation, including its spacing. Your application owns their data and removal actions. Use controls with `type="button"` so removing context does not submit the message.

## Composing custom controls

Use `startActions` to place controls on the left of the toolbar, or before the textarea in the compact variant. Use `endActions` to place controls before the built-in Send or Stop button. Both accept React content, including multiple buttons, a menu or a custom component. Their DOM order follows the visual order for keyboard navigation.

Compose actions with [Button](/web/components/button), [Dropdown](/web/components/dropdown) or your own controls. PromptInput does not provide attachment or microphone buttons because their behavior and labels belong to the application. For icon-only buttons, provide a translated `aria-label` and a tooltip. Use `type="button"` on custom buttons to avoid submitting the message.

`disabled` applies to the textarea and built-in Send or Stop button. Pass the same disabled state to custom controls when they should also be unavailable. PromptInput does not change the properties of custom content. Keep controls concise in the compact variant; use the default variant or move additional actions into a menu when space is limited.


## Demos

### Prompt input

On desktop, Enter sends the message and Shift+Enter adds a new line. On touch-primary devices, Enter adds a new line and the send button submits.


```tsx
render(<PromptInput characterCounter={111} />)
```


### Compact prompt input


```tsx
render(<PromptInput variant="compact" placeholder="Ask Aino" />)
```


### Inline compact prompt input

Use `dropShadow={false}` for the flat inline surface.


```tsx
render(<PromptInput variant="compact" dropShadow={false} placeholder="Ask Aino" />)
```


### Custom actions

Compose attachment and microphone buttons with `startActions` and `endActions`. Here, attachment adds an example document and microphone inserts example dictated text. Connect these controls to your application's file picker and speech recognition.


```tsx
const CustomActions = () => {
  const [value, setValue] = useState('');
  const [hasAttachment, setHasAttachment] = useState(false);
  return <PromptInput variant={variant} icon={null} value={value} onChange={({
    value
  }) => setValue(value)} onSubmit={() => setValue('')} startActions={<Button type="button" variant="tertiary" bounding icon={add} iconSize="default" aria-label="Add attachment" tooltip="Add attachment" disabled={hasAttachment} onClick={() => setHasAttachment(true)} />} endActions={<Button type="button" variant="tertiary" bounding icon={microphone} iconSize="default" aria-label="Use microphone" tooltip="Use microphone" onClick={() => setValue('Show my latest transactions')} />}>
              {hasAttachment && <Button type="button" variant="tertiary" bounding icon={close} iconSize="default" text="Budget.pdf" aria-label="Remove Budget.pdf" onClick={() => setHasAttachment(false)} />}
            </PromptInput>;
};
render(<CustomActions />);
```



```tsx
const CustomActions = () => {
  const [value, setValue] = useState('');
  const [hasAttachment, setHasAttachment] = useState(false);
  return <PromptInput variant={variant} icon={null} value={value} onChange={({
    value
  }) => setValue(value)} onSubmit={() => setValue('')} startActions={<Button type="button" variant="tertiary" bounding icon={add} iconSize="default" aria-label="Add attachment" tooltip="Add attachment" disabled={hasAttachment} onClick={() => setHasAttachment(true)} />} endActions={<Button type="button" variant="tertiary" bounding icon={microphone} iconSize="default" aria-label="Use microphone" tooltip="Use microphone" onClick={() => setValue('Show my latest transactions')} />}>
              {hasAttachment && <Button type="button" variant="tertiary" bounding icon={close} iconSize="default" text="Budget.pdf" aria-label="Remove Budget.pdf" onClick={() => setHasAttachment(false)} />}
            </PromptInput>;
};
render(<CustomActions />);
```


### Prompt input with context

Pass your context as children. The application owns the reference and its removal. The custom action adds it again after removal.


```tsx
const ContextChip = styled.div`
          display: flex;
          align-items: center;
          gap: 0.5rem;
          padding: 0.25rem 0.5rem;
          background: var(--token-color-background-neutral-subtle);
          border: 0.0625rem solid var(--token-color-stroke-neutral-subtle);
          border-radius: 0.5rem;
        `;
const InputWithContext = () => {
  const [hasReference, setHasReference] = useState(true);
  const [value, setValue] = useState('Add the companies from the attached document to my watchlist.');
  return <PromptInput dropShadow value={value} onChange={({
    value
  }) => setValue(value)} onSubmit={() => setValue('')} startActions={<Button type="button" variant="tertiary" size="small" icon={add} iconSize="default" aria-label="Add reference" tooltip="Add reference" disabled={hasReference} onClick={() => setHasReference(true)} />}>
              {hasReference && <ContextChip>
                  <Avatar size="small" src={referenceAvatar} aria-hidden />
                  <span>dnb.no/...Insurance</span>
                  <Button type="button" variant="tertiary" size="small" icon={close} iconSize="default" aria-label="Remove reference" onClick={() => setHasReference(false)} />
                </ContextChip>}
            </PromptInput>;
};
render(<InputWithContext />);
```


### Stop while streaming

Set `isBusy` and pass `onStop` to show a stop button while the reply streams in.


```tsx
const Chat = () => {
  const [isBusy, setIsBusy] = useState(false);
  useEffect(() => {
    if (!isBusy) {
      return; // stop here
    }
    const timeout = setTimeout(() => setIsBusy(false), 4000);
    return () => clearTimeout(timeout);
  }, [isBusy]);
  return <PromptInput variant={variant} isBusy={isBusy} onSubmit={() => setIsBusy(true)} onStop={() => setIsBusy(false)} />;
};
render(<Chat />);
```



```tsx
const Chat = () => {
  const [isBusy, setIsBusy] = useState(false);
  useEffect(() => {
    if (!isBusy) {
      return; // stop here
    }
    const timeout = setTimeout(() => setIsBusy(false), 4000);
    return () => clearTimeout(timeout);
  }, [isBusy]);
  return <PromptInput variant={variant} isBusy={isBusy} onSubmit={() => setIsBusy(true)} onStop={() => setIsBusy(false)} />;
};
render(<Chat />);
```


### Prompt input with an error


```tsx
render(<PromptInput textareaProps={{
  status: 'The message could not be sent. Try again.'
}} />)
```


### Disabled prompt input


```tsx
render(<PromptInput disabled value="Message unavailable" startActions={<Button type="button" variant="tertiary" size="small" text="Add reference" disabled />} />)
```

<!-- eufemia-properties-usage:start -->
Some properties provide flexibility for specific integrations. Follow the component's usage guidance. Customizations beyond the documented patterns need a use case approved by DNB UX. [Read about intended use and customization](/web/styling-and-themes/configuration/component-properties/#intended-use-and-customization).
<!-- eufemia-properties-usage:end -->

## Properties


```json
{
  "props": {
    "dropShadow": {
      "doc": "Add a drop shadow. Defaults to `true` for the compact variant and `false` for the default variant.",
      "type": "boolean",
      "status": "optional"
    },
    "icon": {
      "doc": "Leading icon in the compact variant. Set to `null` to hide it. Defaults to the Aino icon.",
      "type": "IconIcon",
      "status": "optional"
    },
    "children": {
      "doc": "Context shown above the input, such as removable attachment or reference chips.",
      "type": "React.ReactNode",
      "status": "optional"
    },
    "startActions": {
      "doc": "Custom controls at the start of the toolbar, or before the textarea in the compact variant. The application owns their labels, behavior and disabled state.",
      "type": "React.ReactNode",
      "status": "optional"
    },
    "endActions": {
      "doc": "Custom controls before the built-in Send or Stop button. The application owns their labels, behavior and disabled state.",
      "type": "React.ReactNode",
      "status": "optional"
    },
    "variant": {
      "doc": "Use `default` for a multiline card with the send button below the text, or `compact` for an input with a leading icon and inline actions. The compact send button is hidden while the input is empty.",
      "type": [
        "\"default\"",
        "\"compact\""
      ],
      "status": "optional"
    },
    "value": {
      "doc": "The text value. Use it together with `onChange` to control the value. Without it, the text is cleared after submit.",
      "type": "string",
      "status": "optional"
    },
    "placeholder": {
      "doc": "Placeholder text. Defaults to a translated text.",
      "type": "string",
      "status": "optional"
    },
    "label": {
      "doc": "Label for screen readers. Defaults to a translated text.",
      "type": "string",
      "status": "optional"
    },
    "isBusy": {
      "doc": "Prevent submitting while a reply is pending or streaming. Shows a stop button when `onStop` is given.",
      "type": "boolean",
      "defaultValue": "false",
      "status": "optional"
    },
    "characterCounter": {
      "doc": "Show a character counter with the given maximum number of characters. Text that is longer is not sent.",
      "type": "number",
      "status": "optional"
    },
    "disabled": {
      "doc": "Disable the input and built-in Send or Stop button. Disable custom actions separately.",
      "type": "boolean",
      "status": "optional"
    },
    "textareaProps": {
      "doc": "Properties forwarded to the [Textarea](/uilib/components/textarea/properties), e.g. `status` for an error message.",
      "type": "TextareaProps",
      "status": "optional"
    },
    "[Space](/uilib/layout/space/properties)": {
      "doc": "Spacing properties like `top` or `bottom` are supported.",
      "type": [
        "string",
        "object"
      ],
      "status": "optional"
    }
  },
  "usageGuidance": "Some properties provide flexibility for specific integrations. Follow the component's usage guidance. Customizations beyond the documented patterns need a use case approved by DNB UX. [Read about intended use and customization](/web/styling-and-themes/configuration/component-properties/#intended-use-and-customization)."
}
```


## Translations

| Key                 | nb-NO            | en-GB           | sv-SE                | da-DK           |
| ------------------- | ---------------- | --------------- | -------------------- | --------------- |
| `promptLabel`       | Melding          | Message         | Meddelande           | Besked          |
| `promptPlaceholder` | Skriv en melding | Write a message | Skriv ett meddelande | Skriv en besked |
| `send`              | Send             | Send            | Skicka               | Send            |
| `stop`              | Stopp            | Stop            | Stoppa               | Stop            |

## Events


```json
{
  "props": {
    "onChange": {
      "doc": "Called when the text changes. Receives `{ value }`.",
      "type": "function",
      "status": "optional"
    },
    "onSubmit": {
      "doc": "Called with the text when the user presses Enter on desktop or the send button. Receives `{ value, event }`. Empty text is not submitted. Shift+Enter and Enter on touch-primary devices add a new line.",
      "type": "function",
      "status": "optional"
    },
    "onStop": {
      "doc": "Called when the user presses the stop button.",
      "type": "function",
      "status": "optional"
    }
  }
}
```
