Vitral 0.2
Form

Editor

Rich text with its own engine and no dependency: headings, lists, tables, images, links and Markdown shortcuts, bound as sanitised HTML.

Import

main.ts
import { Editor } from '@vitral/vue';

Default

Release notes

This editor writes semantic HTML, keeps undo history and understands Markdown as you type. Try ## , - or **bold**.

  • Paste from Word or Google Docs

  • Select text and press Ctrl+K for a link

Nothing pasted can run a script: only what the schema knows survives.

Rich text. Alt+F10 moves to the toolbar, Escape comes back. Markdown shortcuts such as # and - work at the start of a line.
<script setup lang="ts">import { Editor, Label, StackPanel } from '@vitral/vue';import { ref } from 'vue'; const article = ref(    '<h2>Release notes</h2><p>This editor writes <strong>semantic HTML</strong>, keeps <em>undo</em> history and understands <code>Markdown</code> as you type. Try <code>## </code>, <code>- </code> or <code>**bold**</code>.</p>' +        '<ul data-type="taskList"><li data-type="taskItem" data-checked="true"><p>Paste from Word or Google Docs</p></li><li data-type="taskItem" data-checked="false"><p>Select text and press Ctrl+K for a link</p></li></ul>' +        '<blockquote><p>Nothing pasted can run a script: only what the schema knows survives.</p></blockquote>');</script> <template>    <StackPanel spacing="0.375rem" style="width: 100%">        <Label for="ed-article">Article</Label>        <Editor id="ed-article" v-model="article" placeholder="Write something…" />    </StackPanel></template>

Custom toolbar

`toolbar` takes groups of item names; the `toolbar` slot replaces the bar with parts of your own.

Only the basics here.

Rich text. Alt+F10 moves to the toolbar, Escape comes back. Markdown shortcuts such as # and - work at the start of a line.


Rich text. Alt+F10 moves to the toolbar, Escape comes back. Markdown shortcuts such as # and - work at the start of a line.
<script setup lang="ts">import { Editor, EditorButton, EditorToolbar, EditorToolbarGroup, StackPanel } from '@vitral/vue';import { ref } from 'vue'; const note = ref('<p>Only the basics here.</p>');</script> <template>    <StackPanel spacing="1rem" style="width: 100%">        <Editor v-model="note" :toolbar="[['bold', 'italic', 'strike'], ['bulletList', 'orderedList'], ['link', 'undo', 'redo']]" aria-label="Note" />        <Editor aria-label="Minimal" placeholder="The toolbar slot">            <template #toolbar>                <EditorToolbar>                    <EditorToolbarGroup>                        <EditorButton command="heading2" />                        <EditorButton command="bold" />                        <EditorButton command="italic" />                    </EditorToolbarGroup>                    <EditorToolbarGroup>                        <EditorButton command="clear" show-label />                    </EditorToolbarGroup>                </EditorToolbar>            </template>        </Editor>    </StackPanel></template>

Slash menu and block actions

Type `/` where a word starts and the blocks are offered, filtered as you type; Enter or a press puts one in. Beside the block the caret is in there is a handle, and it opens duplicate, move, turn into text and delete. `slash-menu` and `block-menu` take your own entries, or `false` for none.

Type a slash on the empty line under this one.


Rich text. Alt+F10 moves to the toolbar, Escape comes back. Markdown shortcuts such as # and - work at the start of a line.

This one offers three commands of its own.


Rich text. Alt+F10 moves to the toolbar, Escape comes back. Markdown shortcuts such as # and - work at the start of a line.
<script setup lang="ts">import { Editor, StackPanel } from '@vitral/vue';import { ref } from 'vue'; const blocks = ref('<p>Type a slash on the empty line under this one.</p><p></p>');const custom = ref('<p>This one offers three commands of its own.</p><p></p>'); // A slash command is data: a name, a line about it, and either a command the// editor knows or a `run` of your own.const slashCommands = [    { id: 'heading2', label: 'Section', description: 'A heading for a section', icon: 'heading2', command: ['toggleHeading', 2] as const },    { id: 'taskList', label: 'Checklist', description: 'Things to tick off', icon: 'listChecks', command: ['toggleTaskList'] as const },    {        id: 'today',        label: "Today's date",        description: 'Puts the date in, as text',        icon: 'calendar',        keywords: ['date', 'now'],        run: (editor: { run: (name: string, ...args: unknown[]) => boolean }) => editor.run('insertText', new Date().toLocaleDateString())    }];</script> <template>    <StackPanel spacing="1rem" style="width: 100%">        <Editor v-model="blocks" aria-label="Slash menu example" :toolbar="[['blockType'], ['bold', 'italic']]" />        <Editor            v-model="custom"            aria-label="Own slash commands"            :toolbar="false"            :slash-menu="slashCommands"            :block-menu="[{ id: 'delete', label: 'Delete', icon: 'trash', run: (editor) => editor.run('deleteBlock') }]"        />    </StackPanel></template>

Bubble toolbar

`bubble-menu` shows a toolbar over selected text; with `:toolbar="false"` it is the only one.

Select some of this text: a floating toolbar appears over it, with bold, italic, a link and a colour.

Rich text. Alt+F10 moves to the toolbar, Escape comes back. Markdown shortcuts such as # and - work at the start of a line.
<script setup lang="ts">import { Editor } from '@vitral/vue';import { ref } from 'vue'; const bubble = ref('<p>Select some of this text: a floating toolbar appears over it, with bold, italic, a link and a colour.</p>');</script> <template>    <Editor v-model="bubble" :toolbar="false" bubble-menu aria-label="Bubble toolbar example" style="width: 100%" /></template>

Read only

Terms

Read-only text can still be selected and copied, and links open.

  1. First

  2. Second

Rich text. Alt+F10 moves to the toolbar, Escape comes back. Markdown shortcuts such as # and - work at the start of a line.
<script setup lang="ts">import { Editor } from '@vitral/vue';import { ref } from 'vue'; const terms = ref(    '<h3>Terms</h3><p>Read-only text can still be <mark data-color="yellow">selected and copied</mark>, and <a href="https://example.com" target="_blank">links</a> open.</p><ol><li><p>First</p></li><li><p>Second</p></li></ol>');</script> <template>    <Editor v-model="terms" readonly :toolbar="false" aria-label="Terms" style="width: 100%" /></template>

With count and limit

`max-length` stops typing and pasting at the limit; the count describes the text.

Keep it short.

Rich text. Alt+F10 moves to the toolbar, Escape comes back. Markdown shortcuts such as # and - work at the start of a line.
<script setup lang="ts">import { Editor, Label, StackPanel } from '@vitral/vue';import { ref } from 'vue'; const bio = ref('<p>Keep it short.</p>');</script> <template>    <StackPanel spacing="0.375rem" style="width: 100%">        <Label for="ed-bio">Bio</Label>        <Editor id="ed-bio" v-model="bio" :max-length="140" :toolbar="[['bold', 'italic', 'link']]" />    </StackPanel></template>

HTML and JSON output

Type here and watch both formats change.

Rich text. Alt+F10 moves to the toolbar, Escape comes back. Markdown shortcuts such as # and - work at the start of a line.
<p>Type here and watch <strong>both</strong> formats change.</p>
null
<script setup lang="ts">import { Editor, type EditorJSON } from '@vitral/vue';import { ref } from 'vue'; const output = ref('<p>Type here and watch <strong>both</strong> formats change.</p>');const outputJson = ref<EditorJSON | null>(null);</script> <template>    <Editor v-model="output" v-model:json="outputJson" aria-label="Output example" :toolbar="[['blockType'], ['bold', 'italic', 'color', 'highlight'], ['bulletList', 'link']]" style="width: 100%" />    <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); gap: 1rem; width: 100%">        <pre class="demo-output" aria-label="HTML" tabindex="0" style="margin: 0; max-height: 16rem; overflow: auto; white-space: pre-wrap; font-size: 0.75rem">{{ output }}</pre>        <pre class="demo-output" aria-label="JSON" tabindex="0" style="margin: 0; max-height: 16rem; overflow: auto; font-size: 0.75rem">{{ JSON.stringify(outputJson, null, 2) }}</pre>    </div></template>

Composed from parts

`Editor.Root`, `Editor.Toolbar`, `Editor.Button`, `Editor.Content`, `Editor.BubbleMenu`, `Editor.Footer` and `Editor.Count`, also exported as `EditorRoot`, `EditorToolbar`… share one editor; `useEditor()` reaches it from your own components.

This one is built from parts, with a toolbar of its own and a word count.

Rich text. Alt+F10 moves to the toolbar, Escape comes back. Markdown shortcuts such as # and - work at the start of a line.
<script setup lang="ts">import { Editor } from '@vitral/vue';import { ref } from 'vue'; const composed = ref('<p>This one is built from parts, with a toolbar of its own and a word count.</p>');const words = ref(0);</script> <template>    <Editor.Root v-model="composed" placeholder="Parts…" aria-label="Composed editor" style="width: 100%" @text-change="words = $event.textValue.split(/\s+/).filter(Boolean).length">        <Editor.Toolbar>            <Editor.ToolbarGroup>                <Editor.BlockSelect :options="['paragraph', 'heading1', 'heading2']" />            </Editor.ToolbarGroup>            <Editor.ToolbarGroup>                <Editor.Button command="bold" />                <Editor.Button command="italic" />                <Editor.ColorPicker kind="highlight" />                <Editor.Button command="link" />            </Editor.ToolbarGroup>            <Editor.ToolbarGroup>                <Editor.Button command="taskList" />                <Editor.ImageButton />                <Editor.TableMenu />            </Editor.ToolbarGroup>        </Editor.Toolbar>        <Editor.Content />        <Editor.BubbleMenu :items="['bold', 'italic', 'link']" />        <Editor.Footer>            <span style="margin-inline-end: auto">Edited words: {{ words }}</span>            <Editor.Count :words="false" />        </Editor.Footer>    </Editor.Root></template>

API

Read from packages/vue/src/components/Editor/types.ts, so it says what the component actually accepts.

Props

NameTypeDescription
placeholderstring—
readonlybooleanThe text can be read, selected and copied, not changed.
disabledboolean—
invalidboolean—
variantInputVariantDefaults to the plugin's `inputVariant`.
maxLengthnumberThe most characters the text may hold; typing and pasting stop there.
autofocusbooleanFocuses the text, with the caret at the end, once mounted.
colorsstring[]Palette colour names the colour and highlight pickers offer, in order. Defaults to the whole palette.
historyDelaynumberMilliseconds within which consecutive typing is one undo step. Defaults to 500.
slashMenuboolean | SlashCommand[]The menu a `/` opens where a word starts: `true` for the blocks the editor knows, the commands to offer, or `false` for none. Defaults to true.
blockMenuboolean | BlockAction[]The handle beside the block the caret is in: `true` for the usual actions, the actions to offer, or `false` for none. Defaults to true.
toolbarboolean | EditorToolbarItem[][]The toolbar: `false` for none, or groups of item names, as in `[['bold', 'italic'], ['link']]`. The `toolbar` slot replaces it entirely.
bubbleMenuboolean | EditorToolbarItem[]A floating toolbar over selected text; `true` for the default buttons, or the items to show.
showCountbooleanShows the word and character count under the text. On by default when there is a `maxLength`.
showWordCountbooleanShows the word count beside the character count. Defaults to true.

Plus pt, dt and unstyled from BaseProps, see pass-through and unstyled mode.

Emits

EventPayloadDescription
update:modelValuevalue: string—
update:jsonvalue: EditorJSON—
text-changeevent: EditorTextChangeEventThe content changed.
selection-changeevent: EditorSelectionChangeEvent—
focusevent: FocusEvent—
blurevent: FocusEvent—
loadevent: { instance: unknown }The editor is ready; `instance` is the framework-free editor behind the component.

Slots

NameSlot propsDescription
toolbar—Replaces the toolbar. Build one from `EditorToolbar`, `EditorButton` and the other parts.
footer—Replaces the footer's content (the count).