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

# Rich Text Editor – Code

> RichTextEditor is a rich text editing field with a configurable toolbar supporting text formatting, lists, links, mentions, and media.

export const LiveCode = ({children, customHeight, clickToLoad, example, fullWidth, fullHeight, hideCodeInLiveCode, screenshot, screenshotOnly, showCode: showCodeProp}) => {
  const SCREENSHOTS_BASE = "https://servicetitan.github.io/anvil2-docs-live-code/screenshots";
  const STACKBLITZ_BASE = "https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples";
  const [showCodeBlock, setShowCodeBlock] = useState(showCodeProp ?? false);
  const [isLocalOverride, setIsLocalOverride] = useState(false);
  useEffect(() => {
    const examplePath = `/images/live-code-screenshots-tmp/${example}.png`;
    fetch(examplePath, {
      method: "HEAD"
    }).then(r => {
      if (r.ok) setIsLocalOverride(true);
    }).catch(() => {});
  }, [example]);
  const screenshotBase = isLocalOverride ? "/images/live-code-screenshots-tmp" : SCREENSHOTS_BASE;
  if (screenshotOnly) {
    return <Frame className="flex flex-col">
        <div className="flex dark:hidden" style={{
      justifyContent: "center",
      alignItems: "center",
      width: fullWidth ? "100%" : "50%",
      minHeight: fullHeight ? "284px" : undefined,
      background: "#FFFFFF"
    }}>
          <img srcset={`${screenshotBase}/${example}.png, ${screenshotBase}/${example}-2x.png 2x`} src={`${screenshotBase}/${example}.png`} alt={example} noZoom />
        </div>
        <div className="hidden dark:flex" style={{
      justifyContent: "center",
      alignItems: "center",
      width: fullWidth ? "100%" : "50%",
      minHeight: fullHeight ? "284px" : undefined,
      background: "#141414"
    }}>
          <img srcset={`${screenshotBase}/${example}-dark.png, ${screenshotBase}/${example}-dark-2x.png 2x`} src={`${screenshotBase}/${example}-dark.png`} alt={example} noZoom />
        </div>
      </Frame>;
  }
  if (screenshot) {
    return <Frame className="flex flex-col -mb-2">
        <div className="flex dark:hidden bg-white dark:bg-codeblock border border-gray-950/10 dark:border-white/10 dark:twoslash-dark rounded-2xl overflow-hidden" style={{
      justifyContent: "center",
      alignItems: "center",
      width: fullWidth ? "100%" : "50%",
      minHeight: fullHeight ? "284px" : undefined
    }}>
          <img srcset={`${screenshotBase}/${example}.png, ${screenshotBase}/${example}-2x.png 2x`} src={`${screenshotBase}/${example}.png`} alt={example} noZoom />
        </div>

        <div className="hidden dark:flex bg-white dark:bg-codeblock border border-gray-950/10 dark:border-white/10 dark:twoslash-dark rounded-2xl overflow-hidden" style={{
      background: "#141414",
      justifyContent: "center",
      alignItems: "center",
      width: fullWidth ? "100%" : "50%",
      minHeight: fullHeight ? "284px" : undefined
    }}>
          <img srcset={`${screenshotBase}/${example}-dark.png, ${screenshotBase}/${example}-dark-2x.png 2x`} src={`${screenshotBase}/${example}-dark.png`} alt={example} noZoom />
        </div>

        <div className="flex justify-end items-center text-xs py-2 px-1 gap-4">
          {!showCodeProp ? <button className="inline-flex justify-end items-center text-gray-700 dark:text-gray-50 hover:text-blue-500 dark:hover:text-blue-300 transition-colors group self-end gap-1 cursor-pointer" onClick={() => setShowCodeBlock(!showCodeBlock)} style={{
      appearance: "none"
    }}>
              <Icon icon="code" size="12px" className="group-hover:bg-blue-500 dark:group-hover:bg-blue-300" />
              <span>{showCodeBlock ? "Hide code" : "Show code"}</span>
            </button> : null}

          <a className="inline-flex justify-end items-center hover:text-blue-500 dark:hover:text-blue-300 transition-colors group self-end gap-1" href={`${STACKBLITZ_BASE}/${example}?file=src/App.tsx`} target="_blank" rel="noreferrer">
            <Icon icon="bolt" size="12px" className="group-hover:bg-blue-500 dark:group-hover:bg-blue-300" />
            <span>StackBlitz demo</span>
          </a>
        </div>

        <div className="grid transition-[grid-template-rows] duration-300 ease-in-out overflow-auto overflow-y-hidden overflow-x-auto" style={showCodeBlock ? {
      gridTemplateRows: "1fr"
    } : {
      gridTemplateRows: "0fr"
    }}>
          <div style={{
      minHeight: 0,
      overflowX: "auto",
      overflowY: "hidden",
      marginBlockStart: "-1.25rem",
      marginBlockEnd: "-1.5rem"
    }}>
            {children}
          </div>
        </div>
      </Frame>;
  } else {
    return <div style={{
      display: "flex",
      width: fullWidth ? "100%" : "50%",
      minHeight: customHeight ? customHeight : "316px",
      resize: "vertical",
      overflow: "auto"
    }}>
        <iframe title={example} style={{
      flex: 1,
      width: fullWidth ? "100%" : "50%",
      minHeight: customHeight ? customHeight : "316px"
    }} src={`${STACKBLITZ_BASE}/${example}?embed=1&hideNavigation=1&hideExplorer=1&terminalHeight=0&file=src/App.tsx${clickToLoad ? "&ctl=1" : ""}${hideCodeInLiveCode ? "&view=preview" : ""}`} allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking" sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts" />
      </div>;
  }
};

<Note>
  **Beta Feature**

  This feature is currently in beta, and needs to be imported from `@servicetitan/anvil2/beta`.

  While we hope to minimize breaking changes, they may occur due to feedback we receive or other improvements. These will always be documented in the changelog and communicated in Slack.

  Please reach out in the [#ask-designsystem](https://servicetitan.enterprise.slack.com/archives/CBSRGHTRS) channel with any questions or feedback!
</Note>

<Tabs>
  <Tab title="Implementation">
    ## Live Component Playground

    <LiveCode showCode example="rich-text-editor-playground" fullWidth screenshot />

    ## Common Examples

    ### Basic Usage

    A minimal editor with the default toolbar — text formatting and alignment enabled, everything else off.

    <LiveCode showCode example="rich-text-editor-basic" fullWidth screenshot />

    ### Error and Validation

    Pass an error message string to `error` to display validation feedback below the editor.

    <LiveCode showCode example="rich-text-editor-error" fullWidth screenshot />

    ### Read-only

    Pass `disabled` to prevent editing. The toolbar remains visible but all controls are disabled.

    <LiveCode showCode example="rich-text-editor-readonly" fullWidth screenshot />

    ### With Mentions

    Enable `allowMention` and supply `mentionOptions` to support @mention typeahead. Use `onMentionInputChange` to filter options as the user types, and `renderMentionHoverContent` to show a custom hover card over inserted mentions.

    <LiveCode showCode example="rich-text-editor-with-mentions" fullWidth screenshot />

    ### With Media

    Enable `allowMedia` with a required `onImageUpload` handler to support image uploads and YouTube embeds.

    <LiveCode showCode example="rich-text-editor-with-media" fullWidth screenshot />

    ### All Features

    All toolbar sections enabled together: formatting, alignment, lists, checklist, code, blockquote, link, mentions, drag-and-drop, and media.

    <LiveCode showCode example="rich-text-editor-with-all-features" fullWidth screenshot />

    ## Controlled vs Uncontrolled

    `RichTextEditor` supports both controlled and uncontrolled modes.

    * **Uncontrolled** — Pass `defaultValue` to set initial HTML content. The editor manages its own internal state.
    * **Controlled** — Pass `value` and `onChange` to manage content externally. `onChange` fires on every change with the serialized HTML string.

    Use `onJsonChange` when you need access to TipTap's JSON representation for more granular content handling.

    ## Toolbar Features

    Each toolbar section is opt-in via a corresponding `allow*` prop. Text formatting and alignment are enabled by default; all other sections are disabled unless explicitly enabled.

    | Feature                                                      | Prop                 | Default |
    | ------------------------------------------------------------ | -------------------- | ------- |
    | Text type, font size, bold, italic, underline, strikethrough | `allowTextFormat`    | `true`  |
    | Left, center, right, justify alignment                       | `allowTextAlignment` | `true`  |
    | Ordered and unordered lists, indent controls                 | `allowLists`         | `false` |
    | Checklist (task list)                                        | `allowChecklist`     | `false` |
    | Inline code                                                  | `allowCode`          | `false` |
    | Blockquote                                                   | `allowBlockquote`    | `false` |
    | Link insert and remove                                       | `allowLink`          | `false` |
    | @mention typeahead                                           | `allowMention`       | `false` |
    | Drag handle for block reordering                             | `allowDragDrop`      | `false` |
    | Image upload and YouTube embedding                           | `allowMedia`         | `false` |

    ## Mentions

    Enable @mention support with `allowMention`. Supply `mentionOptions` as an array of `{ id, label }` objects and use `onMentionInputChange` to filter options dynamically as the user types.

    Customize the dropdown item appearance with `renderMentionOption` and the hover card shown over inserted mentions with `renderMentionHoverContent`.

    ## Media

    Enable image and YouTube embedding with `allowMedia`. When `allowMedia` is `true`, `onImageUpload` is required. The callback receives the selected `File[]` and an `onComplete` callback — invoke it with the resolved image URLs once upload finishes.

    ```tsx theme={null}
    <RichTextEditor
      allowMedia
      onImageUpload={(files, onComplete) => {
        uploadFiles(files).then((urls) => onComplete(urls));
      }}
    />
    ```

    Control accepted file types with `allowedMimeTypes`. When omitted, the editor defaults to common image formats.
  </Tab>

  <Tab title="RichTextEditor Props">
    ```tsx theme={null}
    import { RichTextEditor } from "@servicetitan/anvil2/beta";

    <RichTextEditor label="Notes" />
    ```

    ## `RichTextEditor` Props

    The `RichTextEditor` accepts the following props. It also extends `LayoutUtilProps` for spacing and sizing utilities.

    <ParamField path="allowBlockquote" type="boolean" default="false">
      Enables the blockquote toolbar button.
    </ParamField>

    <ParamField path="allowChecklist" type="boolean" default="false">
      Enables the checklist (task list) toolbar button.
    </ParamField>

    <ParamField path="allowCode" type="boolean" default="false">
      Enables the inline code toolbar button.
    </ParamField>

    <ParamField path="allowDragDrop" type="boolean" default="false">
      Enables a drag handle for reordering content blocks.
    </ParamField>

    <ParamField path="allowedMimeTypes" type={"`image/${string}`[]"}>
      MIME types accepted by the file picker and paste/drop handler when `allowMedia` is `true`. Defaults to common image formats when omitted.
    </ParamField>

    <ParamField path="allowLink" type="boolean" default="false">
      Enables the link insert and remove toolbar button.
    </ParamField>

    <ParamField path="allowMedia" type="boolean" default="false">
      Enables the Insert dropdown with Image and YouTube options. When `true`, `onImageUpload` is required.
    </ParamField>

    <ParamField path="allowMention" type="boolean" default="false">
      Enables the @mention toolbar button and typeahead dropdown. Requires `mentionOptions` to populate the dropdown.
    </ParamField>

    <ParamField path="allowTextAlignment" type="boolean" default="true">
      Enables the text alignment toolbar section: left, center, right, and justify.
    </ParamField>

    <ParamField path="allowTextFormat" type="boolean" default="true">
      Enables the text formatting toolbar section: text type dropdown, font size, bold, italic, underline, and strikethrough.
    </ParamField>

    <ParamField path="defaultValue" type="string">
      Initial content for uncontrolled mode. Accepts an HTML string (e.g., `"<p>Hello <strong>world</strong></p>"`) or a plain string, which the editor wraps in a `<p>` tag.
    </ParamField>

    <ParamField path="description" type="ReactNode">
      Description text displayed below the editor.
    </ParamField>

    <ParamField path="disabled" type="boolean" default="false">
      Disables the editor and all toolbar controls.
    </ParamField>

    <ParamField path="error" type={`boolean | string | string[]`}>
      Error state. Pass `true` for error styling only, or a string or string array to display error messages below the editor.
    </ParamField>

    <ParamField path="hint" type="ReactNode">
      Hint text displayed below the editor.
    </ParamField>

    <ParamField path="id" type="string">
      HTML id applied to the editor's contenteditable element.
    </ParamField>

    <ParamField path="label" type="ReactNode">
      Label for the editor field.
    </ParamField>

    <ParamField path="labelProps" type="FieldLabelProps">
      Additional props passed to the label element.
    </ParamField>

    <ParamField path="mentionOptions" type="RichTextEditorMentionOption[]">
      Options displayed in the @mention typeahead dropdown. Each option requires an `id` and `label`.
    </ParamField>

    <ParamField path="moreInfo" type="ReactNode">
      Additional info content rendered alongside the label, typically used for tooltips or help text.
    </ParamField>

    <ParamField path="onChange" type="(html: string) => void">
      Callback fired with the serialized HTML string on each content change.
    </ParamField>

    <ParamField path="onImageUpload" type="(files: File[], onComplete: (srcs: string[]) => void) => void">
      Required when `allowMedia` is `true`. Called when the user selects, pastes, or drops image files. Invoke `onComplete` with the resolved image URLs once upload finishes.
    </ParamField>

    <ParamField path="onJsonChange" type="(json: JSONContent) => void">
      Callback fired with the TipTap JSON representation on each content change.
    </ParamField>

    <ParamField path="onMentionInputChange" type="(query: string) => void">
      Fired when the user types after `@`. Use this to filter or async-load `mentionOptions`.
    </ParamField>

    <ParamField path="placeholder" type="string">
      Placeholder text shown when the editor is empty.
    </ParamField>

    <ParamField path="renderMentionHoverContent" type="(option: RichTextEditorMentionOption) => ReactNode">
      Custom render function for the hover card shown when hovering over an inserted @mention.
    </ParamField>

    <ParamField path="renderMentionOption" type="(option: RichTextEditorMentionOption) => ReactNode">
      Custom render function for each option row in the @mention typeahead dropdown.
    </ParamField>

    <ParamField path="required" type="boolean">
      Marks the field as required and adds a visual indicator to the label.
    </ParamField>

    <ParamField path="value" type="string">
      Controlled HTML content. Use with `onChange` to update the value on each change.
    </ParamField>

    ## `RichTextEditorMentionOption` Type

    A single option displayed in the @mention typeahead dropdown.

    ```tsx theme={null}
    type RichTextEditorMentionOption = {
      id: string;
      label: string;
    };
    ```

    <ParamField path="id" type="string" required>
      Unique identifier for the mention option, stored as an attribute on the inserted mention node.
    </ParamField>

    <ParamField path="label" type="string" required>
      Display name shown in the @mention dropdown and rendered inside the inserted mention node.
    </ParamField>
  </Tab>
</Tabs>
