---
name: whatsapp-editor
category: inputs
summary: "Textarea + formatting toolbar + live preview for WhatsApp messages"
subpath: "@42/core/whatsapp-editor"
deepDoc: docs/llm/whatsapp-editor.md
---

Enhances a `<textarea>` whose value IS the WhatsApp markup you send (`*bold*`,
`_italic_`, `~strike~`, `` `mono` ``, `>` quotes, `-`/`1.` lists). Toolbar +
keyboard shortcuts wrap/unwrap the selection; an optional emoji picker uses the
same `renderPicker` plugin as `textarea-emoji`; a live, HTML-escaped preview
shows how WhatsApp will render it. No styling applied.

```html
<div data-c42-whatsapp-editor>
  <!-- the editor card: toolbar + input + footer (styled via .c42-whatsapp-editor-surface) -->
  <div class="c42-whatsapp-editor-surface">
    <div data-c42-wa-toolbar>
      <button data-c42-wa-command="bold" aria-label="Bold"><b>B</b></button>
      <button data-c42-wa-command="italic" aria-label="Italic"><i>I</i></button>
      <button data-c42-wa-command="strikethrough" aria-label="Strikethrough">S</button>
      <button data-c42-wa-command="monospace" aria-label="Monospace">&lt;/&gt;</button>
      <button data-c42-wa-command="blockquote" aria-label="Quote">&gt;</button>
      <button data-c42-wa-command="bullet" aria-label="Bulleted list">•</button>
      <button data-c42-wa-command="ordered" aria-label="Numbered list">1.</button>
      <button data-c42-wa-command="clear" aria-label="Clear formatting">⌫</button>
    </div>
    <textarea data-c42-wa-input placeholder="Write a message…"></textarea>
    <!-- footer: counter left, emoji affordance bottom-right (picker drops below) -->
    <div class="c42-whatsapp-editor-footer">
      <span data-c42-wa-counter></span>
      <span class="c42-whatsapp-editor-emoji">
        <button data-c42-wa-trigger aria-label="Emoji">🙂</button>
        <div data-c42-wa-picker hidden></div>
      </span>
    </div>
  </div>
  <!-- optional bubble toolbar shown above the selection -->
  <div data-c42-wa-floating class="c42-whatsapp-editor-floating" hidden>
    <button data-c42-wa-command="bold" aria-label="Bold"><b>B</b></button>
    <button data-c42-wa-command="italic" aria-label="Italic"><i>I</i></button>
    <button data-c42-wa-command="strikethrough" aria-label="Strikethrough">S</button>
    <span class="c42-whatsapp-editor-separator" aria-hidden="true"></span>
    <button data-c42-wa-command="monospace" aria-label="Monospace">&lt;/&gt;</button>
  </div>
  <!-- preview lives OUTSIDE the surface, as a separate block below the editor -->
  <div data-c42-wa-preview aria-live="polite"></div>
</div>
```

```ts
import { WhatsappEditor } from '@42/core/whatsapp-editor';
const editor = new WhatsappEditor(root, { maxLength: 4096 });
editor.on('whatsappeditor:change', (e) => sendToApi(e.detail.text)); // text = markup
editor.format('bold'); // toggle markers around the selection
```

Toolbar buttons carry `data-c42-wa-command`. Inline markers
(`bold|italic|strikethrough|monospace`) wrap/unwrap the selection; block kinds
(`blockquote|bullet|ordered`) toggle a line prefix across the selected lines
(numbered lists renumber from 1); `clear` strips all formatting from the
selection (or the whole message if nothing is selected). Keyboard: Cmd/Ctrl+B
(bold), Cmd/Ctrl+I (italic). The picker reflects `data-picker-open` on the root
and closes on outside-click / Escape. The preview escapes all input before
interpreting markers (XSS-safe).

Optional floating selection toolbar: add a `[data-c42-wa-floating]` element with
`[data-c42-wa-command]` buttons anywhere inside the root, **or** pass
`floating: true` to have the controller inject a default themed bubble menu
(bold/italic/strikethrough/monospace) when you don't supply one. Authored markup
always wins; the flag only decides who provides the UI. Either way the controller
shows it centered above the selection (positioned via a mirror-div caret
measurement), hides it on collapse/blur/Escape/scroll/resize, and reflects each
inline marker's active state on every command button (toolbar + floating) via
`aria-pressed` and `data-active`.

Themed layout (`@42/styles`): wrap the toolbar, textarea and footer in
`.c42-whatsapp-editor-surface` to get the editor "card" (white surface, rounded,
subtle shadow, fixed toolbar separated by a divider, seamless borderless input).
The preview is intentionally **outside** that surface — a separate block below
the editor — so it never looks like part of the compose box. Structure is
layout-only; the controller finds its parts by `data-*` regardless.

Options: `value`, `maxLength` (default 4096), `emojis`, `floating`, `renderPicker`
Methods: `format(marker)`, `applyBlock(kind)`, `clearFormatting()`, `insertText(text)`, `getText()`, `getHTML()`, `focus()`, `isFloatingOpen()`, `value` get/set
Events: `whatsappeditor:change` → `{ text, html, length }`, `whatsappeditor:format` → `{ marker, text }`
Pure helpers (no DOM): `toPreviewHTML(text)`, `toggleMarker(value, start, end, marker)`, `toggleLinePrefix(value, start, end, kind)`, `stripFormatting(text)`, `isMarkerActive(value, start, end, marker)`, `WHATSAPP_MARKERS`

Optional device preview (`@42/styles`): wrap the preview in
`.c42-whatsapp-editor-device` to render it on a WhatsApp phone background (image
ships with `@42/styles`). Purely opt-in — omit the wrapper for the plain bubble.
Tune with `--c42-wa-device-pad-top`, `--c42-wa-device-pad-x`, `--c42-wa-device-height`.

```html
<div class="c42-whatsapp-editor-device">
  <div data-c42-wa-preview class="c42-whatsapp-editor-preview"></div>
</div>
```
