Custom blocks
A block is defined with defineBlock(). It bundles five things:
- metadata (
type,label,icon), - default values for a new instance,
- a render component (how it looks on the canvas),
- an inspector schema (the controls in the right panel), and
- a
toHtmlexporter (email-safe HTML).
1. The render component
The component receives values, selected, and editing props and emits update with a partial patch.
vue
<!-- RatingBlock.vue -->
<script setup lang="ts">
defineProps<{ values: { stars: number; color: string }; editing?: boolean }>()
defineEmits<{ update: [patch: Partial<{ stars: number; color: string }>] }>()
</script>
<template>
<div :style="{ textAlign: 'center', fontSize: '26px', color: values.color }">
<span v-for="n in values.stars" :key="n">★</span>
</div>
</template>vue
<!-- RatingBlock.vue -->
<script setup>
defineProps({ values: Object, editing: Boolean })
defineEmits(['update'])
</script>
<template>
<div :style="{ textAlign: 'center', fontSize: '26px', color: values.color }">
<span v-for="n in values.stars" :key="n">★</span>
</div>
</template>2. The block definition
ts
import { defineBlock } from 'vue-mail-editor'
import { Star } from 'lucide-vue-next'
import RatingBlock from './RatingBlock.vue'
export const rating = defineBlock<{ stars: number; color: string }>({
type: 'rating',
label: 'Rating',
icon: Star,
defaultValues: () => ({ stars: 5, color: '#f59e0b' }),
render: RatingBlock,
inspector: [
{
title: 'Rating',
controls: [
{ type: 'number', key: 'stars', label: 'Stars', min: 1, max: 5 },
{ type: 'color', key: 'color', label: 'Color' },
],
},
],
toHtml: (v) =>
`<div style="text-align:center;color:${v.color}">${'★'.repeat(v.stars)}</div>`,
})js
import { defineBlock } from 'vue-mail-editor'
import { Star } from 'lucide-vue-next'
import RatingBlock from './RatingBlock.vue'
export const rating = defineBlock({
type: 'rating',
label: 'Rating',
icon: Star,
defaultValues: () => ({ stars: 5, color: '#f59e0b' }),
render: RatingBlock,
inspector: [
{
title: 'Rating',
controls: [
{ type: 'number', key: 'stars', label: 'Stars', min: 1, max: 5 },
{ type: 'color', key: 'color', label: 'Color' },
],
},
],
toHtml: (v) =>
`<div style="text-align:center;color:${v.color}">${'★'.repeat(v.stars)}</div>`,
})3. Register it
vue
<EmailEditor :blocks="[rating]" />Custom blocks are merged with the built-ins. To hide a built-in, list its type in disabledBlocks:
vue
<EmailEditor :blocks="[rating]" :disabled-blocks="['divider', 'spacer']" />Inspector schema
Each inspector group has a title, optional icon, and a list of controls. Every control has a type, a key (the value it edits — dotted keys like padding.top are supported), and a label. See the full catalog in Inspector controls.
ts
const inspector = [
{
title: 'Style',
controls: [
{ type: 'font', key: 'fontFamily', label: 'Font' },
{ type: 'number', key: 'fontSize', label: 'Size', unit: 'px', min: 8, max: 80 },
{ type: 'color', key: 'color', label: 'Color' },
{ type: 'align', key: 'align', label: 'Align' },
],
},
]js
const inspector = [
{
title: 'Style',
controls: [
{ type: 'font', key: 'fontFamily', label: 'Font' },
{ type: 'number', key: 'fontSize', label: 'Size', unit: 'px', min: 8, max: 80 },
{ type: 'color', key: 'color', label: 'Color' },
{ type: 'align', key: 'align', label: 'Align' },
],
},
]Writing email-safe toHtml
Email clients (especially Outlook) are unforgiving. Keep toHtml to:
- tables for layout, not flexbox/grid,
- inline styles only,
- px units and web-safe fonts (or your own
@font-facewith fallbacks), - absolute image URLs.
toHtml(values, ctx) receives an ExportContext as its second argument for shared concerns (e.g. content width).