Create a Custom Block
This example creates a “sticker” block—a large emoji that can be placed, rotated, and resized on the canvas. It includes a custom toolbar button and a floating menu emoji picker.
<script setup lang="ts">import { WovenCanvas, SelectTool, HandTool, FloatingMenuBar, Toolbar } from '@woven-canvas/vue'import '@woven-canvas/vue/style.css'import { Sticker } from './Sticker'import StickerBlock from './StickerBlock.vue'import StickerTool from './StickerTool.vue'import StickerMenuButton from './StickerMenuButton.vue'
const blockDefs = [ { tag: 'sticker', components: [Sticker], },]</script>
<template> <WovenCanvas :editor="{ components: [Sticker], blockDefs }"> <!-- Toolbar --> <template #toolbar> <Toolbar> <SelectTool /> <HandTool /> <StickerTool /> </Toolbar> </template>
<!-- Floating menu with emoji picker --> <template #floating-menu> <FloatingMenuBar> <template #button:sticker="{ entityIds }"> <StickerMenuButton :entity-ids="entityIds" /> </template> </FloatingMenuBar> </template>
<!-- Block renderer --> <template #block:sticker="props"> <StickerBlock v-bind="props" /> </template> </WovenCanvas></template>import { field } from '@woven-canvas/core'import { defineCanvasComponent } from '@woven-canvas/vue'
export const Sticker = defineCanvasComponent( { name: 'sticker', sync: 'document' }, { emoji: field.string().max(8).default('😀'), },)<script setup lang="ts">import { computed } from 'vue'import { useComponent } from '@woven-canvas/vue'import { Sticker } from './Sticker'
const props = defineProps<{ entityId: number selected: boolean}>()
// Subscribe to sticker data reactivelyconst sticker = useComponent(props.entityId, Sticker)
const containerStyle = computed(() => ({ width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', containerType: 'size' as const, // Subtle shadow when selected filter: props.selected ? 'drop-shadow(0 0 8px rgba(59, 130, 246, 0.5))' : 'none', transition: 'filter 0.15s ease',}))</script>
<template> <div :style="containerStyle"> <span class="emoji">{{ sticker?.emoji }}</span> </div></template>
<style scoped>.emoji { font-size: 90cqmin; line-height: 1; user-select: none;}</style><script setup lang="ts">import { ToolbarButton } from '@woven-canvas/vue'
// Snapshot defines what gets created when the tool is used// Note: position and rank are set automatically, so we only need tag and sizeconst snapshot = JSON.stringify({ block: { tag: 'sticker', size: [120, 120], }, sticker: { emoji: '😀', },})</script>
<template> <ToolbarButton name="sticker" tooltip="Sticker" :placement-snapshot="snapshot" :drag-out-snapshot="snapshot"> <span style="font-size: 20px; line-height: 1">😀</span> </ToolbarButton></template><script setup lang="ts">import { computed } from 'vue'import { MenuDropdown, useComponents, useEditorContext } from '@woven-canvas/vue'import { Sticker } from './Sticker'
const props = defineProps<{ entityIds: number[]}>()
// Available emojis to choose fromconst emojis = ['😀', '😎', '🎉', '⭐', '❤️', '🔥', '👍', '🚀', '🌈', '🎨', '💡', '✨']
// Get sticker data for all selected entitiesconst stickers = useComponents(() => props.entityIds, Sticker)
// Current emoji (first selected, or mixed indicator)const currentEmoji = computed(() => { const values = [...stickers.value.values()].map((s) => s?.emoji) if (values.length === 0) return '😀' const allSame = values.every((e) => e === values[0]) return allSame ? values[0] : '🔀'})
// Editor context for writesconst { nextEditorTick } = useEditorContext()
function setEmoji(emoji: string, close: () => void) { nextEditorTick((ctx) => { for (const entityId of props.entityIds) { const sticker = Sticker.write(ctx, entityId) sticker.emoji = emoji } }) close()}</script>
<template> <MenuDropdown title="Sticker"> <template #button> <div class="emoji-button"> <span class="emoji-display">{{ currentEmoji }}</span> <span class="chevron">▾</span> </div> </template>
<template #dropdown="{ close }"> <div class="emoji-grid"> <button v-for="emoji in emojis" :key="emoji" class="emoji-option" :class="{ 'is-active': currentEmoji === emoji }" @click="setEmoji(emoji, close)" > {{ emoji }} </button> </div> </template> </MenuDropdown></template>
<style scoped>.emoji-button { display: flex; align-items: center; justify-content: center; height: 100%; gap: 8px; padding: 0 8px;}
.emoji-display { font-size: 16px; line-height: 1;}
.chevron { font-size: 12px; line-height: 1;}
.emoji-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 4px; padding: 8px; background: var(--wov-gray-700); border-radius: var(--wov-menu-border-radius);}
.emoji-option { width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; font-size: 18px; border: none; background: transparent; border-radius: 4px; cursor: pointer; transition: background-color 0.15s;}
.emoji-option:hover { background: var(--wov-gray-600);}
.emoji-option.is-active { background: var(--wov-primary);}</style>How It Works
Section titled “How It Works”Sticker.ts — Canvas Component
Section titled “Sticker.ts — Canvas Component”Defines a canvas component to store the sticker’s emoji. The name: "sticker" is used for slot naming (#block:sticker, #button:sticker).
StickerBlock.vue — Block Renderer
Section titled “StickerBlock.vue — Block Renderer”Renders the emoji using useComponent() to reactively read the sticker data. Uses CSS container queries to scale the emoji with the block size.
App.vue — Block Definition
Section titled “App.vue — Block Definition”Registers the block type with tag and components. The #block:sticker slot renders StickerBlock for any block with tag: "sticker".
StickerTool.vue — Toolbar Button
Section titled “StickerTool.vue — Toolbar Button”Creates a toolbar button using ToolbarButton. The placement-snapshot defines what gets created when the tool is used—a block with tag: "sticker" and initial emoji data.
StickerMenuButton.vue — Floating Menu
Section titled “StickerMenuButton.vue — Floating Menu”Adds an emoji picker to the floating menu using MenuButton and MenuDropdown. Uses useComponents() to read data from all selected stickers, and nextEditorTick() to write changes.
The slot #button:sticker only appears when all selected blocks have the Sticker component.
Related
Section titled “Related”- Using the Editor API — Programmatically read and update canvas state
- Create a Plugin — Package custom behavior into a reusable plugin
- Plugins — Plugin structure, systems, commands, and keybinds