@dev_owl/cliveedit

CliveEdit — A themeable WYSIWYG markdown editor component for Vue 3
4,092 2
Install
npm install @dev_owl/cliveedit
Latest Version:0.2.5
Vue:3
License:MIT
Last Updated:Oct 9, 2026
Links: GitHub  ·  npm
Maintainer: dev_owl

CliveEdit

A themeable WYSIWYG markdown editor component for Vue 3, built with TypeScript.

CliveEdit gives your users a rich editing experience with a familiar toolbar while keeping markdown as the canonical data format. Switch between visual and raw markdown modes, undo/redo with full history, and adapt the look to any design system using CSS custom properties.

🚀 Try the Demo


Table of Contents


Installation

npm install @dev_owl/cliveedit

Peer dependency: Vue 3.3 or higher.

npm install vue@^3.5

Quick Start

<template>
  <CliveEdit v-model="content" />
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { CliveEdit } from '@dev_owl/cliveedit'
import '@dev_owl/cliveedit/style.css'

const content = ref('# Hello World\n\nStart writing **markdown** here.')
</script>

That's it. The editor renders a toolbar and a WYSIWYG editing area. The content ref always contains the raw markdown string.


MarkdownViewer (Read-only)

Need to display markdown without editing? Use the MarkdownViewer component:

<template>
  <MarkdownViewer v-model="content" />
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { MarkdownViewer } from '@dev_owl/cliveedit'
import '@dev_owl/cliveedit/style.css'

const content = ref('# Hello World\n\nThis is **read-only** markdown.')
</script>

The viewer renders markdown to styled HTML using the same theming system as the editor. Links are clickable by default.

MarkdownViewer Props

Prop Type Default Description
modelValue string (required) Raw markdown string to render. Use with v-model.
bordered boolean true Show a border around the viewer. Set to false for borderless rendering.
codeCopyButton boolean true Show a copy-to-clipboard button on every code block. Set to false to hide it.
highlightOptions HighlightOptions — Enable Shiki syntax highlighting when the viewer is used standalone. Injected automatically inside CliveEdit.

Borderless Example

<MarkdownViewer v-model="content" :bordered="false" />

Code Block Copy Button

Every code block in the viewer carries a copy button that puts the block's content on the clipboard. It appears when the block is hovered or focused, confirms the copy for a moment, and is always visible on touch devices. Keyboard users reach it by tabbing to it.

This is a viewer-only feature — the editor never shows it, so it can't be confused with editable content.

<!-- On by default -->
<MarkdownViewer v-model="content" />

<!-- Switched off -->
<MarkdownViewer v-model="content" :code-copy-button="false" />

The button is styled with CSS variables like everything else:

Variable Default Description
--ce-code-copy-bg rgba(0, 0, 0, 0.06) Button background
--ce-code-copy-text #6b7280 Button icon colour
--ce-code-copy-hover-bg rgba(0, 0, 0, 0.12) Button background on hover
--ce-code-copy-hover-text #374151 Button icon colour on hover
--ce-code-copy-done-text #16a34a Icon colour while the copy is confirmed

Clipboard writes use the async Clipboard API and fall back to a selection-based copy on insecure origins (plain http).


Props

Prop Type Default Description
modelValue string (required) Raw markdown content. Use with v-model.
mode 'wysiwyg' | 'markdown' 'wysiwyg' Active editing mode. Use with v-model:mode.
placeholder string 'Start writing...' Placeholder text shown when the editor is empty.
disabled boolean false When true, disables all editing and toolbar actions.
toolbarItems ToolbarItem[] (built-in set) Override the default toolbar. See Custom Toolbar.
historyDepth number 100 Maximum number of undo/redo history entries.
stickyToolbar boolean true Keep the toolbar pinned at the top of the viewport when scrolling. Set to false to let it scroll with the content.
highlightOptions { theme?: string; langs?: string[] } undefined Enable syntax highlighting in code blocks via Shiki. See Syntax Highlighting.
emojiPicker boolean | EmojiPickerOptions undefined Enable the emoji picker toolbar button via emoji-picker-element. Pass true for defaults or an options object. See Emoji Picker.
onImageUpload (file: File) => Promise<string> undefined Called when an image is pasted or dropped. Return a URL. If not provided, images are embedded as base64. See Image Paste & Drop.
maxImageSize number 2097152 (2 MB) Maximum image file size in bytes. Images exceeding this are ignored.

Events

Event Payload Description
update:modelValue string Emitted when the markdown content changes. Used by v-model.
update:mode 'wysiwyg' | 'markdown' Emitted when the editing mode switches. Used by v-model:mode.

Usage

<CliveEdit
  v-model="content"
  @update:modelValue="onContentChange"
  @update:mode="onModeChange"
/>
function onContentChange(markdown: string) {
  console.log('Content changed:', markdown)
}

function onModeChange(mode: 'wysiwyg' | 'markdown') {
  console.log('Mode switched to:', mode)
}

v-model Bindings

CliveEdit supports two v-model bindings:

<CliveEdit
  v-model="markdown"
  v-model:mode="editorMode"
/>
Binding Prop Event Description
v-model modelValue update:modelValue Two-way binding for the markdown content.
v-model:mode mode update:mode Two-way binding for the editing mode ('wysiwyg' or 'markdown').

Exposed Methods

Access these via a template ref:

<template>
  <CliveEdit ref="editorRef" v-model="content" />
  <button @click="editorRef?.focus()">Focus Editor</button>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { CliveEdit } from '@dev_owl/cliveedit'

const editorRef = ref<InstanceType<typeof CliveEdit> | null>(null)
const content = ref('')
</script>
Method Signature Description
getMarkdown() () => string Returns the current markdown content.
getMode() () => EditorMode Returns the current editing mode.
undo() () => void Programmatically trigger undo.
redo() () => void Programmatically trigger redo.
print() () => void Open an isolated print view of the current content and trigger the browser print dialog.
focus() () => void Focus the active editor area (WYSIWYG or textarea).

Keyboard Shortcuts

These shortcuts work in both WYSIWYG and Markdown modes:

Shortcut Action
Ctrl+B / Cmd+B Bold
Ctrl+I / Cmd+I Italic
Ctrl+K / Cmd+K Insert link
Ctrl+P / Cmd+P Open print view
Ctrl+Z / Cmd+Z Undo
Ctrl+Shift+Z / Cmd+Shift+Z Redo
Tab (inside list) Indent list item (increase nesting)
Shift+Tab (inside list) Outdent list item (decrease nesting)
Ctrl+Click / Cmd+Click Open link under cursor in a new tab
Tab (inside table) Move to next cell
Shift+Tab (inside table) Move to previous cell
Enter (inside table) Move to same column in next row

Auto-Formatting Shortcuts

In WYSIWYG mode, CliveEdit detects common markdown patterns as you type and automatically converts them into the corresponding rich-text elements. These shortcuts trigger when you press Space after typing the pattern at the very beginning of an empty line (inside a <p> or <div> block).

You type Result
* Creates a bullet list (<ul>)
- Creates a bullet list (<ul>)
1. (or any N. ) Creates an ordered list (<ol>)
# Converts the line to a Heading 1 (<h1>)
## Converts the line to a Heading 2 (<h2>)
### Converts the line to a Heading 3 (<h3>)

Note: Auto-formatting only applies when the shortcut characters are the sole content of the line. If the line already contains other text, the space is inserted normally. These shortcuts do not apply inside existing lists, blockquotes, tables, code blocks, or headings.


Toolbar

Default Toolbar Items

The built-in toolbar includes 20 buttons plus a mode toggle:

Button Icon Action Active When
Bold Bold Toggle <strong> / ** Cursor inside bold text
Italic Italic Toggle <em> / * Cursor inside italic text
Strikethrough Strikethrough Toggle <del> / ~~ Cursor inside strikethrough text
— separator
Heading 1 Heading1 Set/toggle <h1> / # Cursor inside H1
Heading 2 Heading2 Set/toggle <h2> / ## Cursor inside H2
Heading 3 Heading3 Set/toggle <h3> / ### Cursor inside H3
— separator
Bullet List List Toggle <ul> / - Cursor inside unordered list
Ordered List ListOrdered Toggle <ol> / 1. Cursor inside ordered list
Indent List IndentIncrease Nest list item one level deeper —
Outdent List IndentDecrease Un-nest list item one level —
— separator
Blockquote Quote Toggle <blockquote> / > Cursor inside blockquote
Inline Code Code Toggle <code> / ` Cursor inside code
Code Block CodeXml Toggle <pre> / ``` Cursor inside code block
— separator
Link Link Insert <a> / [](https://github.com/Dev-Owl/clive_editor/blob/HEAD/url) Cursor inside link
Image Image Insert <img> / ![alt]() —
Horizontal Rule Minus Insert <hr> / --- —
Emoji Smile Open emoji picker —
Table Table Insert a 3×3 table Cursor inside a table
— separator
Print Printer Open an isolated print view (Ctrl+P) —
— separator
Undo Undo2 Undo last change —
Redo Redo2 Redo last undone change —
— spacer
Mode Toggle FileCode / Eye Switch between WYSIWYG and Markdown mode —

Custom Toolbar

toolbarItems lets you define your own toolbar layout.

Important behavior:

  • toolbarItems replaces the built-in toolbar completely.
  • A toolbar item can be either:
    • a built-in editor action via action: ToolbarAction
    • a custom button via onClick: (ctx) => void
  • If you want to append to the built-in toolbar instead of rewriting it manually, use the exported defaultToolbarItems.

That means:

  • If you want a smaller toolbar, pass only the buttons you want.
  • If you want to add one extra button to the default toolbar, spread defaultToolbarItems and append your own item.
  • Custom buttons can run application-specific logic and can also use the injected EditorContext, including ctx.insertText(...) for literal text and ctx.insertMarkdown(...) for markdown-aware insertion.

Insertion semantics:

  • ctx.insertText(text) always inserts literal text.
  • ctx.insertMarkdown(markdown) inserts raw markdown in markdown mode and renders the markdown into rich content in visual mode before syncing it back to the canonical markdown value.

Example: append application-specific buttons to the built-in toolbar and use both insertion modes.

<template>
  <CliveEdit v-model="content" :toolbar-items="myToolbar" />
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { CliveEdit, defaultToolbarItems } from '@dev_owl/cliveedit'
import type { ToolbarItem } from '@dev_owl/cliveedit'
import { CalendarClock } from '@lucide/vue'

const content = ref('')

const myToolbar: ToolbarItem[] = [
  ...defaultToolbarItems,
  {
    id: 'insert-date-time',
    label: 'Insert Date/Time',
    icon: CalendarClock,
    divider: true,
    onClick: (ctx) => {
      const formatted = new Intl.DateTimeFormat(undefined, {
        dateStyle: 'medium',
        timeStyle: 'short',
      }).format(new Date())

      ctx.insertText(formatted)
    },
  },
  {
    id: 'insert-callout',
    label: 'Insert Callout',
    icon: CalendarClock,
    onClick: (ctx) => {
      ctx.insertMarkdown('> **Note**\n> Review this section before publishing.')
    },
  },
]
</script>

In that example:

  • ctx.insertText(formatted) inserts the formatted date/time as literal text.
  • ctx.insertMarkdown('> **Note**\n> ...') inserts markdown syntax in markdown mode and rendered rich content in visual mode.

Example: build a smaller toolbar from built-in actions only.

<template>
  <CliveEdit v-model="content" :toolbar-items="compactToolbar" />
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { CliveEdit } from '@dev_owl/cliveedit'
import type { ToolbarItem } from '@dev_owl/cliveedit'
import { Bold, Italic, Heading1, Link, Undo2, Redo2 } from '@lucide/vue'

const content = ref('')

const compactToolbar: ToolbarItem[] = [
  {
    id: 'bold',
    label: 'Bold',
    icon: Bold,
    action: 'bold',
    shortcut: 'Ctrl+B',
    active: (ctx) => ctx.isActive('strong'),
  },
  {
    id: 'italic',
    label: 'Italic',
    icon: Italic,
    action: 'italic',
    shortcut: 'Ctrl+I',
    active: (ctx) => ctx.isActive('em'),
  },
  {
    id: 'heading1',
    label: 'Heading 1',
    icon: Heading1,
    action: 'heading1',
    divider: true,
  },
  {
    id: 'link',
    label: 'Link',
    icon: Link,
    action: 'link',
    shortcut: 'Ctrl+K',
    active: (ctx) => ctx.isActive('a'),
  },
  {
    id: 'undo',
    label: 'Undo',
    icon: Undo2,
    action: 'undo',
    divider: true,
  },
  {
    id: 'redo',
    label: 'Redo',
    icon: Redo2,
    action: 'redo',
  },
]
</script>

toolbarItems still replaces the full rendered toolbar, but exporting defaultToolbarItems makes extension practical.

ToolbarItem Interface
type ToolbarItem = BuiltInToolbarItem | CustomToolbarItem

interface BuiltInToolbarItem {
  id: string              // Unique identifier
  label: string           // Accessible label / tooltip
  icon: Component         // Vue component (Lucide icon or custom)
  action: ToolbarAction   // Action name dispatched on click
  onClick?: never
  shortcut?: string       // Keyboard shortcut label (display only)
  active?: (ctx: EditorContext) => boolean   // Active state check
  divider?: boolean       // Show separator before this item
}

interface CustomToolbarItem {
  id: string
  label: string
  icon: Component
  onClick: (ctx: EditorContext) => void
  action?: never
  shortcut?: string
  active?: (ctx: EditorContext) => boolean
  divider?: boolean
}
type ToolbarAction =
  | 'bold'
  | 'italic'
  | 'strikethrough'
  | 'heading1'
  | 'heading2'
  | 'heading3'
  | 'bulletList'
  | 'orderedList'
  | 'indentList'
  | 'outdentList'
  | 'blockquote'
  | 'codeInline'
  | 'codeBlock'
  | 'link'
  | 'image'
  | 'horizontalRule'
  | 'emoji'
  | 'table'
  | 'print'
  | 'undo'
  | 'redo'

Available action names: bold, italic, strikethrough, heading1, heading2, heading3, bulletList, orderedList, indentList, outdentList, blockquote, codeInline, codeBlock, link, image, horizontalRule, emoji, table, print, undo, redo.

Print action: The print action opens the current document in an isolated print view (a hidden iframe styled with the editor's CSS) and triggers the browser's print dialog. It is read-only — the editor content and the host page are never modified — and it works even when the editor is disabled.

In markdown mode, indentList and outdentList follow the same structural rules as visual mode: list items only move one level at a time, and indenting requires a valid previous sibling at the target level.


Syntax Highlighting

CliveEdit supports syntax highlighting in fenced code blocks via Shiki — the same engine that powers VS Code's syntax highlighting.

Installation

Shiki is an optional peer dependency. Install it alongside CliveEdit:

npm install shiki

Without Shiki installed, code blocks render as plain preformatted text — no errors.

Basic Usage

<CliveEdit
  v-model="content"
  :highlight-options="{
    theme: 'github-light',
    langs: ['javascript', 'typescript', 'python', 'html', 'css']
  }"
/>
Option Type Default Description
theme string 'github-light' Shiki theme name. See Shiki Themes.
langs string[] Common web languages Languages to pre-load. Only pre-loaded languages can be highlighted.

Pass {} (empty object) to use defaults with the github-light theme and a set of common languages.

How It Works

  • Markdown mode: Write fenced code blocks with a language identifier as usual:

    ```javascript
    const greeting = 'Hello, world!'
    ```
    
  • WYSIWYG mode: When you insert a code block, a small language label appears in the top-right corner of the block (initially showing "plain text"). Click the label to type a language name (e.g. javascript, python, css). The code is immediately re-highlighted with the correct syntax colours.

  • MarkdownViewer: Highlighted code blocks render automatically when a parent component provides the highlight function (via CliveEdit or useHighlighter).

Theming the Language Label

The language label badge can be customised with CSS variables:

Variable Default Description
--ce-code-lang-bg rgba(0, 0, 0, 0.06) Label background
--ce-code-lang-text #6b7280 Label text colour
--ce-code-lang-font-size 0.75em Label font size

Emoji Picker

CliveEdit supports an emoji picker for inserting native Unicode emoji via emoji-picker-element — a lightweight web component (~12.5 kB min+gz) with built-in search, categories, skin tones, favourites, and accessibility.

Installation

emoji-picker-element is an optional peer dependency. Install it alongside CliveEdit:

npm install emoji-picker-element

Without emoji-picker-element installed, the emoji button simply does not appear in the toolbar — no errors.

Basic Usage

<!-- Defaults -->
<CliveEdit v-model="content" :emoji-picker="true" />

<!-- With options -->
<CliveEdit
  v-model="content"
  :emoji-picker="{ locale: 'en' }"
/>

Options

Option Type Default Description
locale string 'en' Emoji data locale.
dataSource string jsdelivr CDN URL to fetch emoji data from. Useful for self-hosting.

Pass true instead of an object to use all defaults.

How It Works

  • Click the 😊 (Smile) button in the toolbar to open a floating emoji picker panel.
  • Search for emoji by typing in the search box.
  • Browse by category (Smileys, Animals, Food, etc.).
  • Pick a skin tone via the built-in skin tone selector.
  • Recently used emoji appear in the Favourites row.
  • Select an emoji to insert the native Unicode character at the current cursor position. This works in both WYSIWYG and Markdown modes.
  • The picker closes on selection, pressing Escape, or clicking outside.

Emoji are native Unicode characters — they render using each platform's built-in emoji font (Apple on macOS/iOS, Segoe UI Emoji on Windows, Noto Color Emoji on Linux/Android). This is the expected cross-platform behaviour.

Dark Mode

The emoji picker automatically detects whether the editor is in dark mode by reading the computed --ce-bg CSS variable on the .cliveedit wrapper and calculating its luminance. There is no manual darkMode option — just apply your dark theme via CSS custom properties as usual and the picker follows:

<div :class="{ 'dark-theme': isDark }">
  <!-- The emoji picker automatically adapts its dark/light appearance -->
  <CliveEdit v-model="content" :emoji-picker="true" />
</div>

Image Paste & Drop

CliveEdit supports pasting and dragging images directly into the editor. Screenshots from the clipboard, files from your desktop, or images copied from other apps can be inserted seamlessly.

Accepted formats: PNG, JPEG, GIF, WebP

Inserted images are centered automatically in both the editor and the read-only viewer. Resizing only changes the image width, so the browser keeps the original aspect ratio intact.

Default Behaviour (Base64)

With no extra configuration, pasted or dropped images are embedded as base64 data URIs:

<!-- Images just work — no config needed -->
<CliveEdit v-model="content" />

The resulting markdown:

![pasted image](data:image/png;base64,iVBORw0KGgo...)

Note: Base64 encoding increases file size by ~33%. For large images, consider using a custom upload handler instead.

Visual Resize Controls

Click an image in WYSIWYG mode to open its resize tooltip. CliveEdit includes preset width steps of 25%, 50%, 75%, and 100%, plus a custom percentage field for exact sizing.

Resized images are stored in markdown using image-title metadata, for example:

![Preview](https://example.com/image.png "ce-width:75%")

That metadata is handled automatically by CliveEdit and round-trips cleanly between visual and markdown modes.

Custom Upload Handler

Provide an onImageUpload callback to upload images to your own server or cloud storage. The function receives a File object and should return a Promise<string> with the final URL:

<template>
  <CliveEdit v-model="content" :on-image-upload="uploadImage" />
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { CliveEdit } from '@dev_owl/cliveedit'

const content = ref('')

async function uploadImage(file: File): Promise<string> {
  const formData = new FormData()
  formData.append('image', file)

  const res = await fetch('/api/upload', {
    method: 'POST',
    body: formData,
  })
  const { url } = await res.json()
  return url  // e.g. 'https://cdn.example.com/images/photo.png'
}
</script>

If the upload function throws an error, the image is silently discarded.

Size Limit

By default, images larger than 2 MB are rejected. Customise this with the maxImageSize prop (value in bytes):

<!-- Allow images up to 5 MB -->
<CliveEdit
  v-model="content"
  :max-image-size="5 * 1024 * 1024"
/>

Tables

CliveEdit includes full GFM (GitHub Flavoured Markdown) table support. Tables round-trip cleanly between WYSIWYG and Markdown modes.

Inserting a Table

Click the Table button in the toolbar (or call ctx.table() programmatically) to insert a 3×3 starter table with a header row and two body rows. The table is rendered as standard HTML <table> markup and serialised to pipe-delimited GFM markdown:

| Header | Header | Header |
| --- | --- | --- |
|  |  |  |
|  |  |  |

Table Controls

When the cursor is inside a table, a floating control bar appears above it with the following actions:

Group Button Description
Row ↑ + Add a row above the current row
Row ↓ + Add a row below the current row
Row ↓ − Remove the current row (disabled for header rows and the last body row)
Col ← + Add a column to the left
Col → + Add a column to the right
Col → − Remove the current column (disabled when only one column remains)
— 🗑 Delete the entire table

The control bar repositions automatically as the selection moves between tables and hides when the cursor leaves all tables.

Table Keyboard Navigation

Key Behaviour
Tab Move to the next cell (left → right, then next row). If at the last cell, a new row is appended.
Shift+Tab Move to the previous cell.
Enter Move to the same column in the next row. If already on the last row, a new row is appended.

Formatting commands (bold, italic, etc.) work within individual cells. Block-level commands (headings, lists, blockquotes) are disabled when the cursor is inside a table to prevent structural corruption.


Theming

CliveEdit uses CSS custom properties scoped under the .cliveedit class. Override any variable to match your application's design system — no JavaScript configuration needed.

CSS Custom Properties

Layout
Variable Default Description
--ce-bg #ffffff Editor background
--ce-text #1a1a1a Text colour
--ce-border #d0d5dd Border colour
--ce-radius 8px Border radius
Toolbar
Variable Default Description
--ce-toolbar-bg #f9fafb Toolbar background
--ce-toolbar-border #e5e7eb Toolbar bottom border
--ce-toolbar-btn-hover #e5e7eb Button hover background
--ce-toolbar-btn-active #d0d5dd Button active/pressed background
Typography
Variable Default Description
--ce-font-family System font stack Content font family
--ce-font-size 16px Content font size
--ce-line-height 1 Content line height
--ce-mono-font Consolas, monospace... Monospace font for code
Headings
Variable Default Description
--ce-h1-font-size 2em H1 font size
--ce-h1-font-weight 700 H1 font weight
--ce-h1-margin 0.67em 0 H1 margin
--ce-h1-line-height 1.25 H1 line height
--ce-h2-font-size 1.5em H2 font size
--ce-h2-font-weight 600 H2 font weight
--ce-h2-margin 0.75em 0 H2 margin
--ce-h2-line-height 1.3 H2 line height
--ce-h3-font-size 1.25em H3 font size
--ce-h3-font-weight 600 H3 font weight
--ce-h3-margin 0.75em 0 H3 margin
--ce-h3-line-height 1.35 H3 line height
Content Elements
Variable Default Description
--ce-content-margin 0.75em 0 Shared vertical margin for blocks (pre, blockquote, table)
--ce-content-padding 16px 20px Editor & viewer area padding
--ce-code-bg #f3f4f6 Code background
--ce-code-text #e11d48 Inline code text colour
--ce-code-font-size 0.9em Code font size
--ce-code-padding 2px 5px Inline code padding
--ce-code-radius 3px Inline code border radius
--ce-pre-padding 12px 16px Code block padding
--ce-pre-radius 6px Code block border radius
--ce-code-lang-bg rgba(0, 0, 0, 0.06) Code block language label background
--ce-code-lang-text #6b7280 Code block language label text colour
--ce-code-lang-font-size 0.75em Code block language label font size
--ce-code-copy-bg rgba(0, 0, 0, 0.06) Viewer code copy button background
--ce-code-copy-text #6b7280 Viewer code copy button icon colour
--ce-code-copy-hover-bg rgba(0, 0, 0, 0.12) Viewer code copy button background on hover
--ce-code-copy-hover-text #374151 Viewer code copy button icon colour on hover
--ce-code-copy-done-text #16a34a Viewer code copy button icon colour while confirming
--ce-blockquote-border #6366f1 Blockquote left border colour
--ce-blockquote-border-width 4px Blockquote left border width
--ce-blockquote-padding 4px 16px Blockquote padding
--ce-blockquote-opacity 0.85 Blockquote text opacity
--ce-link-color #2563eb Link colour
--ce-list-padding-left 1.5em List indentation
--ce-hr-color #d0d5dd Horizontal rule colour
--ce-hr-border-width 2px Horizontal rule thickness
--ce-hr-margin 1em 0 Horizontal rule margin
--ce-img-radius 4px Image border radius
--ce-table-cell-padding 6px 12px Table cell padding
Interaction
Variable Default Description
--ce-selection-bg rgba(99, 102, 241, 0.2) Text selection highlight
--ce-placeholder-color #9ca3af Placeholder text colour
--ce-focus-ring rgba(99, 102, 241, 0.4) Focus outline colour

Dark Theme Example

.dark-theme .cliveedit {
  --ce-bg: #1e1e1e;
  --ce-text: #d4d4d4;
  --ce-border: #3e3e42;
  --ce-toolbar-bg: #252526;
  --ce-toolbar-border: #3e3e42;
  --ce-toolbar-btn-hover: #3e3e42;
  --ce-toolbar-btn-active: #505054;
  --ce-code-bg: #2d2d30;
  --ce-code-text: #ce9178;
  --ce-blockquote-border: #6366f1;
  --ce-link-color: #4fc1ff;
  --ce-selection-bg: rgba(38, 79, 120, 0.5);
  --ce-placeholder-color: #6a6a6a;
  --ce-focus-ring: rgba(99, 102, 241, 0.5);
  --ce-hr-color: #3e3e42;
  --ce-line-height: 1;
}
<div :class="{ 'dark-theme': isDark }">
  <CliveEdit v-model="content" />
</div>

You can also use @media (prefers-color-scheme: dark) for automatic dark mode:

@media (prefers-color-scheme: dark) {
  .cliveedit {
    --ce-bg: #1e1e1e;
    --ce-text: #d4d4d4;
    /* ... */
  }
}

Vue Plugin (Global Registration)

Instead of importing CliveEdit in every component, you can register it globally:

// main.ts
import { createApp } from 'vue'
import { CliveEditPlugin } from '@dev_owl/cliveedit'
import '@dev_owl/cliveedit/style.css'
import App from './App.vue'

const app = createApp(App)
app.use(CliveEditPlugin)
app.mount('#app')

Then use <CliveEdit> or <MarkdownViewer> anywhere without importing:

<template>
  <CliveEdit v-model="markdown" />
  <MarkdownViewer v-model="markdown" />
</template>

Advanced Usage

EditorContext (Provide / Inject)

CliveEdit provides an EditorContext object via Vue's provide/inject system. Child or descendant components can access the editor's commands and state:

import { inject } from 'vue'
import { EDITOR_CTX_KEY } from '@dev_owl/cliveedit'

const ctx = inject(EDITOR_CTX_KEY)

// Call formatting commands
ctx?.bold()
ctx?.heading(2)
ctx?.link('https://example.com', 'Example')
ctx?.table()  // Insert a 3×3 table
ctx?.insertText(new Date().toISOString())
ctx?.insertMarkdown('**Prefilled** _markdown_')

// Check state
ctx?.isActive('strong') // true if cursor is inside bold text
ctx?.isActive('table')  // true if cursor is inside a table
ctx?.canUndo            // true if undo is available
ctx?.mode               // 'wysiwyg' | 'markdown'

// Switch modes
ctx?.toggleMode()
EditorContext Interface
interface EditorContext {
  // State
  mode: EditorMode
  disabled: boolean

  // Formatting commands
  bold(): void
  italic(): void
  strikethrough(): void
  heading(level: 1 | 2 | 3): void
  bulletList(): void
  orderedList(): void
  indentList(): void
  outdentList(): void
  blockquote(): void
  codeInline(): void
  codeBlock(lang?: string): void
  link(url?: string, text?: string): void
  image(src?: string, alt?: string): void
  horizontalRule(): void
  table(): void
  emoji(): void
  insertText(text: string): void
  insertMarkdown(markdown: string): void

  // History
  undo(): void
  redo(): void
  canUndo: boolean
  canRedo: boolean

  // Mode
  toggleMode(): void

  // Query
  isActive(tag: string): boolean
}

useHistory Composable

The undo/redo system is available as a standalone composable for headless or custom editor builds:

import { useHistory } from '@dev_owl/cliveedit'

const history = useHistory({ maxDepth: 50, debounceMs: 200 })

history.init('# Initial content')
history.pushState('# Updated content')    // debounced (for keystrokes)
history.pushImmediate('# After toolbar')  // immediate (for actions)

const prev = history.undo()   // returns HistoryEntry | null
const next = history.redo()   // returns HistoryEntry | null

history.canUndo.value  // boolean (computed ref)
history.canRedo.value  // boolean (computed ref)
Method Description
init(markdown) Set the initial state. Clears all history.
pushState(markdown) Debounced push — suited for continuous typing.
pushImmediate(markdown) Immediate push — suited for toolbar actions, mode switches.
undo() Returns the previous HistoryEntry or null.
redo() Returns the next HistoryEntry or null.
canUndo Computed ref — true when undo is available.
canRedo Computed ref — true when redo is available.
clear() Clear all history and cancel pending debounce.

useEditor Composable

The editor command layer is available standalone for building custom UIs around a contenteditable element:

import { ref } from 'vue'
import { useEditor } from '@dev_owl/cliveedit'

const editorEl = ref<HTMLElement | null>(null)
const editor = useEditor(editorEl)

// Formatting
editor.bold()
editor.italic()
editor.heading(2)
editor.link('https://example.com')

// State
editor.isActive('strong')  // boolean
editor.refreshActiveState()

// DOM access
editor.getHtml()
editor.setHtml('<p>Hello</p>')
editor.focus()

useHighlighter Composable

The syntax highlighting system is available as a standalone composable for advanced or custom integrations:

import { useHighlighter } from '@dev_owl/cliveedit'

const { init, highlight, isReady, provideHighlight } = useHighlighter()

// Initialise Shiki (async — call once at startup)
await init({ theme: 'github-dark', langs: ['javascript', 'python'] })

// Check readiness
isReady.value  // true after successful init

// Highlight code (synchronous after init)
const html = highlight('const x = 42', 'javascript')

// Provide the highlight function to child components
provideHighlight()
Method / Property Description
init(options?) Initialise Shiki with the given theme and languages. Returns Promise<boolean>.
highlight(code, lang) Highlight code synchronously. Returns '' before init.
isReady Ref<boolean> — true when Shiki is loaded.
highlightFn Ref<HighlightFn | null> — reactive highlight function reference.
provideHighlight() Call in setup() to provide the highlight function to descendants via inject.

Child components can consume the highlight function:

import { useInjectHighlight } from '@dev_owl/cliveedit'

const highlightFn = useInjectHighlight()
// highlightFn.value is null until a parent calls provideHighlight()

useEmojiPicker Composable

The emoji picker system is available as a standalone composable for advanced or custom integrations:

import { useEmojiPicker } from '@dev_owl/cliveedit'

const { init, isReady, enabled, setEnabled } = useEmojiPicker()

// Initialise (async — attempts to load emoji-picker-element)
await init()

// Check readiness
isReady.value  // true after successful init

// Control the feature
setEnabled(false)   // hide the emoji button

Note: Dark mode is detected automatically from the editor's background — there is no manual setDarkMode() call.

Method / Property Description
init(options?) Load emoji-picker-element and enable the feature. Returns Promise<boolean>.
isReady Ref<boolean> — true when the module is loaded.
enabled Ref<boolean> — true when the feature is active.
setEnabled(value) Enable / disable without re-loading.

TypeScript

All types are exported for full IntelliSense support:

import type {
  EditorMode,
  ToolbarAction,
  ToolbarItem,
  HistoryEntry,
  CliveEditProps,
  CliveEditEmits,
  EditorContext,
  HighlightOptions,
  EmojiPickerOptions,
  MarkdownViewerProps,
} from '@dev_owl/cliveedit'

Build Scripts

Script Command Description
npm run dev vite --config vite.dev.config.ts Start the development playground
npm run build Type-check → Vite build → Generate .d.ts Full production build
npm run build:only vite build Build without type-checking
npm run typecheck vue-tsc --noEmit Type-check only

Build outputs in dist/:

File Format Description
cliveedit.es.js ESM For modern bundlers (Vite, Webpack 5, Rollup)
cliveedit.umd.js UMD For script tags and legacy bundlers
editor.css CSS All editor styles (import as @dev_owl/cliveedit/style.css)
*.d.ts TypeScript Type declarations for all exports

Browser Support

CliveEdit uses standard DOM APIs (contenteditable, Selection, Range, DOMParser) and works in all modern browsers:

  • Chrome / Edge 90+
  • Firefox 90+
  • Safari 15+

License

MIT

Related Packages

@abhinavakhil/penna-vue

Vue 3 bindings for @abhinavakhil/penna-core

307 0
@voithos-labs/aragonite-vue

vue component for aragonite, the cute markdown editor lib

195 39
@hmfw/tiptap-editor

基于 Tiptap + Vue 3 的富文本编辑器组件。

523 0
@geelato/lite-editor

A lightweight, extensible rich text editor based on TipTap and Vue 3

3,428 0
@hebamuh28/mini-editor

A lightweight rich text editor component for Vue 3

1,383

Version History

Version Released Vue License
0.2.5 >=3.3.0 MIT
0.2.4 >=3.3.0 MIT
0.2.3 >=3.3.0 MIT
0.2.2 >=3.3.0 MIT
0.2.0 >=3.3.0 MIT