> ## Documentation Index
> Fetch the complete documentation index at: https://docs.corti.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Dictation Web Component

> Drop-in web component for real-time speech-to-text with built-in UI

The `@corti/dictation-web` package provides a set of custom elements that handle microphone management, audio streaming, transcript display, and voice commands on top of the Corti API. It works with any frontend framework or plain HTML.

<Info>
  **Package:** [@corti/dictation-web on npm](https://www.npmjs.com/package/@corti/dictation-web) | **Examples:** [corti-examples/dictation](https://github.com/corticph/corti-examples/tree/main/dictation) | **Live demo:** [CodePen](https://codepen.io/hccullen/pen/OPJmxQR)
</Info>

The library provides two usage modes:

1. **`<corti-dictation>`** -- opinionated, all-in-one component with built-in UI (recommended for most use cases)
2. **Modular components** -- individual building blocks (`<dictation-root>`, `<dictation-recording-button>`, etc.) for fully custom layouts

## Installation

<CodeGroup>
  ```bash npm theme={null}
  npm install @corti/dictation-web
  ```

  ```bash yarn theme={null}
  yarn add @corti/dictation-web
  ```

  ```bash pnpm theme={null}
  pnpm add @corti/dictation-web
  ```

  ```html CDN theme={null}
  <script type="module" src="https://cdn.jsdelivr.net/npm/@corti/dictation-web/dist/bundle.js"></script>
  ```
</CodeGroup>

### Module import

```ts theme={null}
// Side-effect import -- registers all custom elements
import "@corti/dictation-web";

// Named import -- access component classes directly
import { CortiDictation } from "@corti/dictation-web";
```

## Quick start

```html theme={null}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Corti Dictation</title>
    <script type="module" src="https://cdn.jsdelivr.net/npm/@corti/dictation-web/dist/bundle.js"></script>
</head>
<body>
    <corti-dictation id="dictation"></corti-dictation>

    <script>
        const dictation = document.getElementById("dictation");

        dictation.addEventListener("ready", () => {
            console.log("Dictation component is ready");
        });

        dictation.addEventListener("transcript", (event) => {
            const { text, isFinal } = event.detail.data;
            console.log(`${isFinal ? "Final" : "Interim"}: ${text}`);
        });

        // Set your access token (obtained from your backend)
        dictation.accessToken = "<your-access-token>";

        // Configure dictation (required)
        dictation.dictationConfig = {
            primaryLanguage: "en",
            automaticPunctuation: true,
            spokenPunctuation: true,
        };
    </script>
</body>
</html>
```

<Tip>The component handles microphone permissions, device selection, and audio streaming automatically. You only need to provide authentication and listen for events.</Tip>

***

## Configuration

Set `dictationConfig` as a JavaScript property to configure the transcription engine. The type matches the [`config` message](/api-reference/transcribe) from the Transcribe WebSocket API.

```js theme={null}
const dictation = document.querySelector("corti-dictation");

dictation.dictationConfig = {
    primaryLanguage: "en",
    automaticPunctuation: true,
};
```

See the [Transcribe API Reference](/api-reference/transcribe) for the full configuration schema.

***

## Modular components

For custom UI layouts, use individual components inside a `<dictation-root>` parent:

<Warning>All modular components **require** a `<dictation-root>` parent to provide context. They cannot be used standalone.</Warning>

```html theme={null}
<dictation-root id="root" accessToken="<your-access-token>">
    <div class="custom-layout">
        <dictation-recording-button></dictation-recording-button>
        <dictation-settings-menu settingsEnabled="device,language,keybinding"></dictation-settings-menu>
    </div>
</dictation-root>

<script>
    const root = document.getElementById("root");
    root.addEventListener("transcript", (e) => {
        console.log(e.detail.data.text);
    });
</script>
```

| Component                         | Description                                                                                                                                                                               |
| :-------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `<dictation-root>`                | Context provider. Same properties as `<corti-dictation>` (auth, config, devices, keybindings). Add `noWrapper` attribute to remove default styling.                                       |
| `<dictation-recording-button>`    | Start/stop button with audio visualization. Supports `allowButtonFocus`. Has `startRecording()`, `stopRecording()`, `toggleRecording()`, `openConnection()`, `closeConnection()` methods. |
| `<dictation-settings-menu>`       | Settings panel with device and language selectors. Supports `settingsEnabled`.                                                                                                            |
| `<dictation-device-selector>`     | Standalone device dropdown. Supports `disabled`.                                                                                                                                          |
| `<dictation-language-selector>`   | Standalone language dropdown. Supports `disabled`.                                                                                                                                        |
| `<dictation-keybinding-selector>` | Keybinding configuration. Supports `keybindingType` (`"push-to-talk"` or `"toggle-to-talk"`) and `disabled`.                                                                              |

See the [API Reference](/sdk/dictation/reference) for full per-component property and method tables.

***

## Keyboard shortcuts

The component supports two keybinding modes:

| Mode               | Behavior                            | Default key |
| :----------------- | :---------------------------------- | :---------- |
| **Push-to-talk**   | Hold key to record, release to stop | `Space`     |
| **Toggle-to-talk** | Press to start, press again to stop | `Enter`     |

Configure via attributes:

```html theme={null}
<corti-dictation
    settingsEnabled="device,language,keybinding"
    pushToTalkKeybinding="Space"
    toggleToTalkKeybinding="Enter"
></corti-dictation>
```

Keys are specified as `event.key` names (e.g. `"Space"`, `"k"`, `"Meta"`) or `event.code` values (e.g. `"KeyK"`, `"Backquote"`). Modifier combinations are not supported.

<Info>When both keybindings are set to the same key, toggle-to-talk takes priority.</Info>

### Preventing keybinding activation

Use the `keybinding-activated` event to conditionally block keybindings:

```js theme={null}
dictation.addEventListener("keybinding-activated", (event) => {
    if (document.activeElement.tagName === "TEXTAREA") {
        event.preventDefault(); // Don't trigger recording while typing
    }
});
```

***

## Voice commands

Voice commands let users control your application by speaking phrases. Configure commands via `dictationConfig.commands` — each command has a `phrases` pattern and optional `variables`. The component emits a `command` event when a phrase is matched. See the [Transcribe API reference](/api-reference/transcribe) for the full command schema.

<CodeGroup>
  ```js title="index.js" expandable theme={null}
  import { deleteLastSentence, deleteLastWord, getActiveTextarea, hideInterim, insertAtSelection, selectLastSentence, selectLastWord, setActiveTextarea, setCommandOutput, showInterim } from "./helpers.js";

  // Assumes you already have:
  // - dictation (the Dictation Web Component instance)
  // - textareas (array of editable fields)
  // - activeIndex (index of the currently active field)

  const ui = {
    interimTranscript: document.getElementById("interimTranscript"),
    commandOutput: document.getElementById("commandOutput"),
  };

  dictation.dictationConfig = {
    primaryLanguage: "en",
    spokenPunctuation: true,
    automaticPunctuation: false,
    commands: [
      {
        id: "go_to_section",
        phrases: ["go to {section_key} section"],
        variables: [
          {
            key: "section_key",
            type: "enum",
            enum: ["subjective", "objective", "assessment", "plan", "next", "previous"],
          },
        ],
      },
      {
        id: "delete_range",
        phrases: ["delete {delete_range}"],
        variables: [
          {
            key: "delete_range",
            type: "enum",
            enum: ["everything", "the last word", "the last sentence", "that"],
          },
        ],
      },
      {
        id: "select_range",
        phrases: ["select {select_range}"],
        variables: [
          {
            key: "select_range",
            type: "enum",
            enum: ["all", "the last word", "the last sentence"],
          },
        ],
      },
    ],
  };

  dictation.addEventListener("transcript", (e) => {
    const { data } = e.detail;
    const textarea = getActiveTextarea({ textareas, activeIndex });

    if (data.isFinal) {
      insertAtSelection(textarea, `${data.text} `);
      hideInterim(ui);
      return;
    }

    showInterim(ui, data.rawTranscriptText);
  });

  dictation.addEventListener("command", (e) => {
    const { data } = e.detail;
    hideInterim(ui);

    if (data.id === "go_to_section") {
      const section = data.variables.section_key.toLowerCase();

      if (section === "next") {
        ({ activeIndex } = setActiveTextarea({ textareas, activeIndex }, activeIndex + 1));
        setCommandOutput(ui, `Command: ${data.id} (section: ${section})`);
        return;
      }

      if (section === "previous") {
        ({ activeIndex } = setActiveTextarea({ textareas, activeIndex }, activeIndex - 1));
        setCommandOutput(ui, `Command: ${data.id} (section: ${section})`);
        return;
      }

      const index = textareas.findIndex((el) => el.id.toLowerCase() === section);
      if (index !== -1) ({ activeIndex } = setActiveTextarea({ textareas, activeIndex }, index));

      setCommandOutput(ui, `Command: ${data.id} (section: ${section})`);
      return;
    }

    if (data.id === "delete_range") {
      const range = data.variables.delete_range.toLowerCase();
      const textarea = getActiveTextarea({ textareas, activeIndex });

      if (range === "everything") {
        textarea.value = "";
        setCommandOutput(ui, `Command: ${data.id} (range: ${range})`);
        return;
      }

      if (range === "the last word") {
        textarea.value = deleteLastWord(textarea.value);
        setCommandOutput(ui, `Command: ${data.id} (range: ${range})`);
        return;
      }

      if (range === "the last sentence") {
        textarea.value = deleteLastSentence(textarea.value);
        setCommandOutput(ui, `Command: ${data.id} (range: ${range})`);
        return;
      }

      if (range === "that") {
        const start = textarea.selectionStart ?? 0;
        const end = textarea.selectionEnd ?? 0;

        textarea.value =
          start !== end ? textarea.value.slice(0, start) + textarea.value.slice(end) : deleteLastWord(textarea.value);

        setCommandOutput(ui, `Command: ${data.id} (range: ${range})`);
        return;
      }
    }

    if (data.id === "select_range") {
      const range = data.variables.select_range.toLowerCase();
      const textarea = getActiveTextarea({ textareas, activeIndex });

      if (range === "all") {
        textarea.focus();
        textarea.setSelectionRange(0, textarea.value.length);
        setCommandOutput(ui, `Command: ${data.id} (range: ${range})`);
        return;
      }

      if (range === "the last word") {
        selectLastWord(textarea);
        setCommandOutput(ui, `Command: ${data.id} (range: ${range})`);
        return;
      }

      if (range === "the last sentence") {
        selectLastSentence(textarea);
        setCommandOutput(ui, `Command: ${data.id} (range: ${range})`);
        return;
      }
    }
  });
  ```

  ```js title="helpers.js" theme={null}
  export function getActiveTextarea(state) {
    return state.textareas[state.activeIndex];
  }

  export function setActiveTextarea(state, nextIndex) {
    const index = Math.max(0, Math.min(nextIndex, state.textareas.length - 1));
    state.textareas[index]?.focus();
    return { activeIndex: index };
  }

  export function hideInterim(ui) {
    ui.interimTranscript?.classList.add("hidden");
  }

  export function showInterim(ui, text) {
    if (!ui.interimTranscript) return;
    ui.interimTranscript.classList.remove("hidden");
    ui.interimTranscript.textContent = text;
  }

  export function setCommandOutput(ui, text) {
    if (!ui.commandOutput) return;
    ui.commandOutput.textContent = text;
  }

  export function insertAtSelection(textarea, text) {
    const start = textarea.selectionStart ?? 0;
    const end = textarea.selectionEnd ?? 0;

    textarea.value = textarea.value.slice(0, start) + text + textarea.value.slice(end);

    const cursor = start + text.length;
    textarea.setSelectionRange(cursor, cursor);
  }

  export function deleteLastWord(text) {
    const words = text.trim().split(/\s+/);
    words.pop();
    return words.length ? `${words.join(" ")} ` : "";
  }

  export function deleteLastSentence(text) {
    const sentences = text.match(/[^.!?]+[.!?]*\s*/g) ?? [];
    sentences.pop();
    return sentences.join("");
  }

  export function selectLastWord(textarea) {
    const value = textarea.value;
    const trimmed = value.trimEnd();
    const lastSpace = trimmed.lastIndexOf(" ");
    const start = lastSpace === -1 ? 0 : lastSpace + 1;
    textarea.focus();
    textarea.setSelectionRange(start, trimmed.length);
  }

  export function selectLastSentence(textarea) {
    const value = textarea.value;
    const sentences = value.match(/[^.!?]+[.!?]*\s*/g) ?? [];
    if (!sentences.length) return;

    const start = sentences.slice(0, -1).reduce((acc, s) => acc + s.length, 0);
    const end = start + sentences[sentences.length - 1].length;

    textarea.focus();
    textarea.setSelectionRange(start, end);
  }
  ```
</CodeGroup>

***

## Attribute formatting

| Type    | Format                           | Example                                 |
| :------ | :------------------------------- | :-------------------------------------- |
| Boolean | Presence = true, absence = false | `<corti-dictation debug-display-audio>` |
| String  | Attribute value                  | `accessToken="token"`                   |
| Array   | Comma-separated                  | `settingsEnabled="device,language"`     |
| Object  | JavaScript property only         | `dictation.authConfig = { ... }`        |

***

## Resources

* **[npm package](https://www.npmjs.com/package/@corti/dictation-web)** -- latest version and install info
* **[Examples](https://github.com/corticph/corti-examples/tree/main/dictation/typescript/web-component)** -- basic, custom UI, styling, and token refresh examples
* **[CodePen demo](https://codepen.io/hccullen/pen/OPJmxQR)** -- interactive live demo
* **[API Reference](/sdk/dictation/reference)** -- properties, methods, and events for every component
* **[Authentication Guide](/sdk/dictation/authentication)** -- access tokens and automatic refresh
* **[Proxy Guide](/sdk/dictation/proxy)** -- route WebSocket traffic through your own server
* **[Styling Guide](/sdk/dictation/styling)** -- CSS custom properties and theming
* **[Transcribe API Reference](/api-reference/transcribe/)** -- underlying WebSocket API specification

***

<Note>For support or questions, reach out through [help.corti.app](https://help.corti.app)</Note>
