Shopware Design

Text Editor

Source

Usage

Text Editor lets users author rich text content such as descriptions, notes, or formatted documents, binding the HTML string with v-model. Use the customButtons and excludedButtons props to tailor the toolbar (or set show-toolbar to false to hide it entirely), switch between WYSIWYG and raw HTML editing through v-model:code-mode, enable is-inline-edit for an inline experience with a floating toolbar, and extend the editor with custom Tiptap extensions through the tipTapConfig prop.

import { MtTextEditor } from "@shopware-ag/meteor-component-library";

Examples

Inline editing

Enables inline editing with a floating toolbar.

Hidden toolbar

Hide the toolbar completely for a simpler editing experience or a custom toolbar.

Code mode

Start the editor in raw HTML editing mode.

Two-way code mode binding

Control the editor mode programmatically with v-model:code-mode.

Custom toolbar buttons

Add custom buttons to the toolbar, backed by a custom Tiptap extension.

Security gate on initial load

When the initial HTML would change after parsing, an overlay blocks WYSIWYG editing until the diff is reviewed and accepted.

Slots

button_<name>

A dynamic slot is rendered for every toolbar button as button_<name>. Use it to replace the automatically rendered button with your own component. For example, to customize the text-color button:

<template>
  <mt-text-editor v-model="content">
    <template #button_text-color="{ editor, disabled, button }">
      <mt-text-editor-toolbar-button
        :button="button"
        :editor="editor"
        :disabled="disabled"
        @click="openColorPickerModal"
      />

      <ColorPickerModal
        v-if="showColorPickerModal"
        v-model="color"
        @confirm="applyTextColor"
        @cancel="closeColorPickerModal"
      />
    </template>
  </mt-text-editor>
</template>

contextual-buttons

Custom buttons for the editor footer. These can change contextually based on the editor's current state.

Customize the left or right sections of the editor's footer.

Toolbar buttons

The editor includes these built-in buttons by default. Use the excludedButtons prop to remove buttons and customButtons to add your own. The position values are useful when inserting custom buttons (see Positioning).

Button nameDescriptionAlignmentPosition
formatOpens a popover with formatting options.left1000
text-colorAllows the user to pick a text color.left2000
boldToggles bold text.left3000
italicToggles italic text.left4000
underlineToggles underlined text.left5000
strikethroughToggles strikethrough text.left6000
superscriptToggles superscript text.left7000
subscriptToggles subscript text.left8000
text-alignmentOpens a popover to set text alignment.left9000
unordered-listToggles an unordered list.left10000
numbered-listToggles a numbered list.left11000
linkOpens a modal to insert or edit links.left12000
tableOpens a modal to insert or modify tables.left13000
undoUndoes the last action.right1000
redoRedoes the last undone action.right2000
toggle-codeToggles between WYSIWYG mode and raw HTML editing.right3000

Custom buttons

Add custom buttons to the toolbar by passing an array of CustomButton objects to the customButtons prop. Each button supports these properties:

  • name (required): A unique identifier for the button.
  • label (required): The visible label, as direct text or a translation key.
  • icon: An optional icon name from the Meteor icon set, shown instead of the label.
  • isActive: A function returning whether the button is currently active (for example, when bold is toggled on).
  • action: A function that runs when the button is clicked, where you apply an editor command.
  • children: An array of child buttons to build a dropdown or multi-level menu.
  • alignment: Whether the button appears on the left or right of the toolbar.
  • position: The button's order in the toolbar. Lower values appear first.
  • disabled: A function returning whether the button should be disabled.
  • contextualButtons: A function returning additional footer buttons based on the editor's state.

Simple button

A custom button that toggles bold formatting:

<template>
  <mt-text-editor v-model="content" :custom-buttons="customButtons" />
</template>

<script setup>
  import { ref } from "vue";

  const content = ref("<p>Your content here</p>");

  const customButtons = [
    {
      name: "custom-bold",
      label: "Bold",
      icon: "regular-bold-xs",
      position: 3500,
      action: (editor) => {
        editor.chain().focus().toggleBold().run();
      },
      isActive: (editor) => editor.isActive("bold"),
    },
  ];
</script>

Use the children property to build a dropdown. Each child is another CustomButton:

<template>
  <mt-text-editor v-model="content" :custom-buttons="customButtons" />
</template>

<script setup>
  import { ref } from "vue";

  const content = ref("<p>Your content here</p>");

  const customButtons = [
    {
      name: "custom-format",
      label: "Format",
      icon: "regular-style-xs",
      position: 1500,
      children: [
        {
          name: "custom-bold",
          label: "Bold",
          icon: "regular-bold-xs",
          action: (editor) => {
            editor.chain().focus().toggleBold().run();
          },
          isActive: (editor) => editor.isActive("bold"),
        },
        {
          name: "custom-italic",
          label: "Italic",
          icon: "regular-italic-xs",
          action: (editor) => {
            editor.chain().focus().toggleItalic().run();
          },
          isActive: (editor) => editor.isActive("italic"),
        },
      ],
    },
  ];
</script>

Use the contextualButtons property to return footer buttons that appear based on the editor's state, such as a "Remove link" action that only shows when a link is selected:

<template>
  <mt-text-editor v-model="content" :custom-buttons="customButtons" />
</template>

<script setup>
  import { ref } from "vue";

  const content = ref("<p>Your content here</p>");

  const customButtons = [
    {
      name: "custom-link",
      label: "Link",
      icon: "regular-link-xs",
      position: 12500,
      action: (editor) => {
        const url = prompt("Enter the URL");
        if (url) {
          editor.chain().focus().setLink({ href: url }).run();
        }
      },
      contextualButtons: (editor) => {
        if (!editor.isActive("link")) {
          return [];
        }

        return [
          {
            name: "remove-link",
            label: "Remove Link",
            icon: "regular-times-xs",
            action: (editor) => {
              editor.chain().focus().unsetLink().run();
            },
          },
        ];
      },
    },
  ];
</script>

Disabling a button by state

Provide a disabled function to disable a button dynamically, for example when the editor is empty:

<script setup>
  const customButtons = [
    {
      name: "custom-bold",
      label: "Bold",
      icon: "regular-bold-xs",
      position: 3500,
      action: (editor) => {
        editor.chain().focus().toggleBold().run();
      },
      isActive: (editor) => editor.isActive("bold"),
      disabled: (editor) => editor.getText().length === 0,
    },
  ];
</script>

Positioning

Buttons are sorted by their position value, lowest first. The default buttons use increments of 1000, leaving room to insert custom buttons at specific positions. For example, position: 3500 places a button between bold (3000) and italic (4000). See the Toolbar buttons table for the default positions.

Customizing with Tiptap extensions

The editor's features are built on Tiptap extensions. Pass custom extensions through the tipTapConfig prop, except the hardcoded content, editorProps, and onUpdate properties:

<template>
  <mt-text-editor v-model="content" :tip-tap-config="tipTapConfig" />
</template>

<script setup>
  import { Underline } from "@tiptap/extension-underline";

  const tipTapConfig = {
    extensions: [Underline],
  };
</script>

Validating before save

The component exposes a validate() method through a template ref. It checks the current code editor content against what Tiptap can represent and shows the diff modal if the HTML differs. It returns true when valid (no diff, or not in code mode) and false when the diff modal was shown. This is useful for validating content before closing a modal or saving:

<template>
  <mt-text-editor ref="editor" v-model="content" />
  <mt-button @click="onSave">Save</mt-button>
</template>

<script setup>
  import { ref } from "vue";

  const editor = ref(null);
  const content = ref("<p>Your content here</p>");

  const onSave = async () => {
    if (!editor.value) return;
    const isValid = await editor.value.validate();
    if (!isValid) return;
    // proceed with save
  };
</script>

API reference

Props

PropTypeDefault
model-value *string""
error
An error in your business logic related to this field.
Record<string, any>null
label
A label for your text field. Usually used to guide the user what value this field controls.
stringnull
disabled
Add disabled state to the editor
booleanfalse
placeholder
Add placeholder text to the editor
string""
is-inline-edit
Enable inline edit mode
booleanfalse
tip-tap-config
Add custom configuration for the tip tap editor
Record<string, any>{}
custom-buttons
Custom buttons to be added to the toolbar
CustomButton[][]
excluded-buttons
Excluded buttons from the toolbar
string[][]
show-toolbar
Control toolbar visibility. Set to false to hide the toolbar completely.
booleantrue
code-mode
Control code editor mode. Set to true to show the code editor instead of WYSIWYG.
booleanfalse

Events

EventPayload
update:modelValueany[]
update:codeModeany[]

Slots

SlotBindings
contextual-buttons{ editor: Editor; buttons: Reactive<CustomButton[]>; }
footer-left{ editor: Editor; }
footer-right{ editor: Editor; }

Exposed

NameType
validate() => Promise<boolean>

Best practices

Do
  • Use v-model to keep the HTML content in sync with your state.
  • Provide a label so users understand what the field is for.
  • Tailor the toolbar with customButtons and excludedButtons to match the editing needs of the context.
  • Use the code mode when users need to inspect or edit the raw HTML.
Don't
  • Do not feed unsupported HTML markup without expecting the security gate to require a diff review first.
  • Do not rely on the editor for plain, unformatted text where a simpler input is enough.
  • Do not hide the toolbar unless you provide another way to apply formatting.

Behavior

  • The editor is built on Tiptap and accepts custom extensions through tipTapConfig, except the hardcoded content, editorProps, and onUpdate properties.
  • When the editor mounts in WYSIWYG mode, it dry-runs a Tiptap parse and compares the result with the initial HTML. If the HTML differs, an overlay blocks editing until the user reviews a diff and accepts the parsed result. While the gate is active, update:modelValue emits are suppressed.
  • When switching from code mode to WYSIWYG, the editor compares the code with Tiptap's parsed output and shows a side-by-side diff modal if changes are detected. The user can accept the changes and switch, or stay in the code editor.
  • The component exposes a validate() method through a template ref. It checks the current code editor content against what Tiptap can represent and shows the diff modal if the HTML differs. It returns true when valid (no diff or not in code mode) and false when the diff modal was shown, which is useful before saving or closing a parent modal.
  • Custom buttons are ordered by their position value, with default buttons spaced in increments of 1000 so custom buttons can be inserted between them.
  • The footer shows a live character count, and contextual-buttons can change based on the current cursor position.

Accessibility

  • Provide a label so the field has an accessible name.
  • When replacing default buttons with custom ones, ensure each button keeps a clear, descriptive label.