Image
The Image extension provides a full-featured image node with resizable handles, two placement models (float with text wrap, or Notion-style alignment), file upload via paste and drag-and-drop, an interactive URL popover, markdown input rule support, and defense-in-depth XSS protection. The extension ships as a separate package (@domternal/extension-image).
When to use
Section titled “When to use”Choose Image when you need:
- Drag-corner resize handles (most editors lock you to original dimensions or charge for resize)
- Paste from clipboard or drag-and-drop upload via a custom
uploadHandler - Float-with-text-wrap (left, center, right) for magazine-style layouts
- Notion-style alignment (left, center, right, with the text staying below the picture) in the Notion preset, or wherever
placement: 'align'is configured - Defense-in-depth XSS protection at four layers: parse, render, command, and input rule
Skip it if:
- Your editor only renders static images from server-rendered HTML
- You need cropping, filters, or AI editing (out of scope; pair with a separate library)
- Your editor is inline-only and images break the inline flow (configure
inline: true)
Live Playground
Section titled “Live Playground”Click an image to select it and see the resize handles. Use the float buttons to wrap text around the image.
With the default theme enabled. Click the image toolbar button to open the URL popover, or select an image to see the bubble menu with float controls.
Plain image without the theme. The buttons above the editor are custom HTML buttons wired to image commands like setImage(), setImageFloat(), deleteImage().
Installation
Section titled “Installation”pnpm add @domternal/extension-imageimport { Document, Paragraph, Text } from '@domternal/core';import { Image } from '@domternal/extension-image';import { DomternalEditor } from '@domternal/vanilla';import '@domternal/theme';
const dm = new DomternalEditor(document.getElementById('editor')!, { extensions: [ Document, Paragraph, Text, Image.configure({ // Optional: handle file uploads uploadHandler: async (file) => { const form = new FormData(); form.append('file', file); const res = await fetch('/api/upload', { method: 'POST', body: form }); const { url } = await res.json(); return url; }, }), ], content: '<p>Check out this image:</p><img src="https://placehold.co/400x200" alt="Placeholder">',});With the theme, clicking the toolbar image button opens a URL/file popover. Paste or drag images to upload them via the uploadHandler. Select an image to see float controls in the bubble menu (requires BubbleMenu).
import { Component, signal } from '@angular/core';import { DomternalEditorComponent, DomternalToolbarComponent } from '@domternal/angular';import { Editor, Document, Paragraph, Text } from '@domternal/core';import { Image } from '@domternal/extension-image';
@Component({ selector: 'app-editor', imports: [DomternalEditorComponent, DomternalToolbarComponent], templateUrl: './editor.html',})export class EditorComponent { editor = signal<Editor | null>(null); extensions = [Document, Paragraph, Text, Image]; content = '<p>Check out this image:</p><img src="https://placehold.co/400x200" alt="Placeholder">';}@if (editor(); as ed) { <domternal-toolbar [editor]="ed" />}<domternal-editor [extensions]="extensions" [content]="content" (editorCreated)="editor.set($event)"/>The toolbar auto-renders the image insert button. Add <domternal-bubble-menu> with [contexts]="{ image: true }" for float controls on selected images.
import { Domternal } from '@domternal/react';import { Document, Paragraph, Text } from '@domternal/core';import { Image } from '@domternal/extension-image';
export default function Editor() { return ( <Domternal extensions={[ Document, Paragraph, Text, Image.configure({ uploadHandler: async (file) => { const form = new FormData(); form.append('file', file); const res = await fetch('/api/upload', { method: 'POST', body: form }); const { url } = await res.json(); return url; }, }), ]} content='<p>Check out this image:</p><img src="https://placehold.co/400x200" alt="Placeholder">' > <Domternal.Toolbar /> <Domternal.Content /> <Domternal.BubbleMenu contexts={{ image: true }} /> </Domternal> );}The <Domternal.Toolbar /> auto-renders the image insert button. Add <Domternal.BubbleMenu> with contexts={{ image: true }} for float controls on selected images.
<script setup lang="ts">import { Domternal } from '@domternal/vue';import { Document, Paragraph, Text } from '@domternal/core';import { Image } from '@domternal/extension-image';
const extensions = [ Document, Paragraph, Text, Image.configure({ uploadHandler: async (file) => { const form = new FormData(); form.append('file', file); const res = await fetch('/api/upload', { method: 'POST', body: form }); const { url } = await res.json(); return url; }, }),];</script>
<template> <Domternal :extensions="extensions" content='<p>Check out this image:</p><img src="https://placehold.co/400x200" alt="Placeholder">'> <Domternal.Toolbar /> <Domternal.Content /> <Domternal.BubbleMenu :contexts="{ image: true }" /> </Domternal></template>The <Domternal.Toolbar /> auto-renders the image insert button. Add <Domternal.BubbleMenu> with :contexts="{ image: true }" for float controls on selected images.
import { Editor, Document, Paragraph, Text } from '@domternal/core';import { Image } from '@domternal/extension-image';
const editor = new Editor({ element: document.getElementById('editor')!, extensions: [Document, Paragraph, Text, Image],});
// Insert an image by URLeditor.commands.setImage({ src: 'https://placehold.co/400x200', alt: 'Placeholder' });
// Float an imageeditor.commands.setImageFloat('left');editor.commands.setImageFloat('none');
// Delete the selected imageeditor.commands.deleteImage();Schema
Section titled “Schema”| Property | Value |
|---|---|
| ProseMirror name | image |
| Type | Node |
| Group | block (default) or inline (when inline: true) |
| Content | None (atom node) |
| Atom | Yes |
| Draggable | Yes |
| HTML tag | <img> (parses img[src] - requires src attribute) |
Image is an atom node - it cannot contain text or other nodes. It is draggable by default. When selected, resize handles appear at the four corners.
Options
Section titled “Options”| Option | Type | Default | Description |
|---|---|---|---|
inline | boolean | false | When true, images appear inline within paragraphs instead of as block elements |
allowBase64 | boolean | true | Allow data:image/ base64 URLs. When false, data: URLs are blocked entirely |
HTMLAttributes | Record<string, unknown> | {} | HTML attributes added to the <img> element |
uploadHandler | (file: File) => Promise<string> | null | null | Async function that uploads a file and returns the URL. Enables paste/drop upload with loading placeholders |
allowedMimeTypes | string[] | See below | Allowed MIME types for file upload |
maxFileSize | number | 0 | Maximum file size in bytes. 0 means unlimited |
onUploadStart | (file: File) => void | null | null | Called when upload starts for a file |
onUploadError | (error: Error, file: File) => void | null | null | Called when upload fails |
placement | 'float' | 'align' | null | null | Which placement control the bubble menu offers. null follows the editor: the Notion preset offers align, everything else offers float (details) |
Default allowed MIME types:
['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml', 'image/avif']Placement: float or align
Section titled “Placement: float or align”The bubble menu offers exactly one placement control per editor. float wraps text around the picture; align only moves the picture within the measure and keeps the text below it, which is what Notion does. Offering both would ask the author to choose between two things that look the same until the text beside the picture is long enough to tell them apart.
By default (placement: null) the control follows the editor’s preset: an editor created with preset: 'notion' (or carrying the dm-notion-mode class) offers the three align buttons, everything else offers the four float buttons. Set the option explicitly to pin one regardless of preset:
// A classic editor that should place pictures the Notion wayImage.configure({ placement: 'align' })
// A Notion-styled editor that still wants text wrapImage.configure({ placement: 'float' })Whichever control is on offer, both attributes stay part of the schema, so a document written under one setting keeps its layout when opened under the other. The commands are also always available: setImageFloat works in an align editor and vice versa.
Inline images
Section titled “Inline images”import { Image } from '@domternal/extension-image';
// Images appear within text flow instead of as separate blocksconst InlineImage = Image.configure({ inline: true });When inline: true, the image node’s group changes from block to inline, allowing images inside paragraphs alongside text.
File upload
Section titled “File upload”import { Image } from '@domternal/extension-image';
const UploadImage = Image.configure({ uploadHandler: async (file) => { const formData = new FormData(); formData.append('image', file); const res = await fetch('/api/upload', { method: 'POST', body: formData }); return (await res.json()).url; }, maxFileSize: 5 * 1024 * 1024, // 5 MB onUploadStart: (file) => { console.log(`Uploading: ${file.name}`); }, onUploadError: (error, file) => { console.error(`Upload failed for ${file.name}:`, error.message); },});When uploadHandler is provided:
- Paste an image from clipboard to upload and insert it
- Drag and drop an image file onto the editor to upload and insert it
For paste and drop, a decoration-based placeholder appears in the document during upload. On success, the placeholder is replaced with the real image node. On failure, the placeholder is removed and onUploadError is called. The onUploadStart callback fires for each file.
Attributes
Section titled “Attributes”| Attribute | Type | Default | Description |
|---|---|---|---|
src | string | null | null | Image URL (validated for XSS) |
alt | string | null | null | Alternative text for accessibility |
title | string | null | null | Tooltip text on hover |
width | string | null | null | Image width in pixels. A bare number, a numeric string and a px-suffixed string (300, '300', '300px') all size identically, on screen and in every export. |
height | string | null | null | Image height in pixels |
loading | 'lazy' | 'eager' | null | null | Browser lazy-loading hint |
crossorigin | 'anonymous' | 'use-credentials' | null | null | CORS policy for the image |
float | 'none' | 'left' | 'right' | 'center' | 'none' | Text wrapping behavior |
align | 'none' | 'left' | 'center' | 'right' | 'none' | Position within the measure, text stays below (the Notion behavior) |
Default values (null for most, 'none' for float and align) are omitted from the rendered HTML.
float and align are one choice per image: setting either through its command clears the other, so a node never carries both. They are deliberately separate attributes rather than one attribute styled differently per preset, because the document has to record which of the two the author actually saw. An export reads attributes, not presets, and could not otherwise tell a wrapped picture from an aligned one.
Float rendering
Section titled “Float rendering”The float attribute controls how text wraps around the image:
| Float | CSS output |
|---|---|
none | No inline style (default) |
left | float: left; margin: 0 1em 1em 0; |
right | float: right; margin: 0 0 1em 1em; |
center | display: block; margin-left: auto; margin-right: auto; |
Align rendering
Section titled “Align rendering”The align attribute only moves the picture inside the measure; nothing ever comes up beside it. It renders as a data-align attribute plus inline styles, so the same HTML lands in the right place in a plain browser with no theme loaded:
| Align | HTML output |
|---|---|
none | Nothing (default) |
left | data-align="left" + display: block; width: fit-content; margin-right: auto; |
center | data-align="center" + display: block; width: fit-content; margin-left: auto; margin-right: auto; |
right | data-align="right" + display: block; width: fit-content; margin-left: auto; |
Parsing reads align from the data-align attribute alone, never from the style: the centered form is written with the same auto margins float: 'center' uses, and a document that already carries a centered float must keep meaning that rather than acquire an alignment as well.
Unlike a floated image, an aligned image has no 60% width cap, since nothing has to fit beside it. In Notion mode the theme also neutralizes any left-over float from a classic document (the picture renders as its aligned equivalent), because the float controls are not on offer there and a layout the author cannot reach should not persist visually.
Commands
Section titled “Commands”setImage
Section titled “setImage”Insert an image at the current selection.
editor.commands.setImage({ src: 'https://example.com/photo.jpg' });
// With all optionseditor.commands.setImage({ src: 'https://example.com/photo.jpg', alt: 'A scenic view', title: 'Photo by Alice', width: 400, height: 300, loading: 'lazy', crossorigin: 'anonymous', float: 'left',});If an image is already selected (NodeSelection), it is replaced with the new image. Returns false if the URL fails XSS validation or the cursor is inside a code block.
setImageFloat
Section titled “setImageFloat”Change the float attribute of a selected image. Also clears align, since the two placements are one choice.
editor.commands.setImageFloat('left');editor.commands.setImageFloat('center');editor.commands.setImageFloat('none');Returns false if no image is selected or the value is invalid.
setImageAlign
Section titled “setImageAlign”Position a selected image within the measure without wrapping text around it. Also clears float.
editor.commands.setImageAlign('left');editor.commands.setImageAlign('center');editor.commands.setImageAlign('right');editor.commands.setImageAlign('none');Returns false if no image is selected or the value is invalid.
deleteImage
Section titled “deleteImage”Delete the currently selected image.
editor.commands.deleteImage();
// With chainingeditor.chain().focus().deleteImage().run();Keyboard shortcuts
Section titled “Keyboard shortcuts”Image does not register any keyboard shortcuts. Use the toolbar button or the markdown input rule to insert images.
Input rules
Section titled “Input rules”| Input | Result |
|---|---|
 | Image with alt text |
 | Image with alt text and title |
Markdown-style image syntax. The title can be wrapped in single quotes, double quotes, or curly (smart) quotes.
The input rule validates the URL for XSS before inserting.
Toolbar items
Section titled “Toolbar items”Main toolbar
Section titled “Main toolbar”Image registers a button in the toolbar with the name image in group insert at priority 150.
| Item | Command | Icon | Event |
|---|---|---|---|
| Insert Image | setImage | image | insertImage |
Clicking the toolbar button emits an insertImage event which opens the image popover (URL input + file browser).
Slash / floating menu
Section titled “Slash / floating menu”Image registers one item in the Media group of the slash command and floating menu at priority 200, matched by the keywords image, picture, photo, img:
| Item | Label | Description |
|---|---|---|
image | Image | Upload or embed with a link |
The item inserts nothing by itself: it emits the same insertImage event as the toolbar button, so picking Image opens the image popover and the node is created when you apply a URL or pick a file. The popover is part of the extension’s own plugin, so it works with no extra setup. Because the menu passes no anchor element, the popover opens at the caret instead of under the toolbar button.
Bubble menu
Section titled “Bubble menu”Image registers bubble menu items with bubbleMenu: 'image' for placement, alt-text editing, and deletion. The placement set depends on the resolved placement: a classic editor gets the four float controls, a Notion-preset editor gets the three align controls.
Float set (classic editors):
| Item | Command | Icon | Label |
|---|---|---|---|
imageFloatNone | setImageFloat('none') | textIndent | Inline |
imageFloatLeft | setImageFloat('left') | textAlignLeft | Float left |
imageFloatCenter | setImageFloat('center') | textAlignCenter | Center |
imageFloatRight | setImageFloat('right') | textAlignRight | Float right |
Align set (Notion preset, or placement: 'align'):
| Item | Command | Icon | Label |
|---|---|---|---|
imageAlignLeft | setImageAlign('left') | textAlignLeft | Align left |
imageAlignCenter | setImageAlign('center') | textAlignCenter | Align center |
imageAlignRight | setImageAlign('right') | textAlignRight | Align right |
Both sets are followed by the shared actions:
| Item | Command | Icon | Label |
|---|---|---|---|
editImage | emits editImage | textAa | Edit alt text |
deleteImage | deleteImage | trash | Delete |
These items have toolbar: false (hidden from the main toolbar) and bubbleMenu: 'image' (shown only when an image is selected). editImage opens the alt-text popover (see Editing alt text) and shows as active when the selected image already has non-empty alt text.
Add BubbleMenu and use DomternalBubbleMenu with an explicit context map so it only shows for image selections:
import { Document, Paragraph, Text, BubbleMenu } from '@domternal/core';import { Image } from '@domternal/extension-image';import { DomternalEditor, DomternalBubbleMenu } from '@domternal/vanilla';import '@domternal/theme';
const bubbleEl = document.createElement('div');bubbleEl.className = 'dm-bubble-menu';
const dm = new DomternalEditor(document.getElementById('editor')!, { extensions: [ Document, Paragraph, Text, Image, BubbleMenu.configure({ element: bubbleEl }), ],});
new DomternalBubbleMenu(bubbleEl, { editor: dm.editor, contexts: { image: ['imageFloatLeft', 'imageFloatCenter', 'imageFloatRight', '|', 'editImage', 'deleteImage'], },});Use <domternal-bubble-menu> with the contexts input. The component automatically discovers items with bubbleMenu: 'image':
<domternal-bubble-menu [editor]="editor()!" [contexts]="{ image: true }"/>You can also specify which items to show per context:
<domternal-bubble-menu [editor]="editor()!" [contexts]="{ image: ['imageFloatLeft', 'imageFloatCenter', 'imageFloatRight', '|', 'editImage', 'deleteImage'] }"/>Use <Domternal.BubbleMenu> with the contexts prop. The component automatically discovers items with bubbleMenu: 'image':
<Domternal.BubbleMenu contexts={{ image: true }} />You can also specify which items to show per context:
<Domternal.BubbleMenu contexts={{ image: ['imageFloatLeft', 'imageFloatCenter', 'imageFloatRight', '|', 'editImage', 'deleteImage'], }}/>Use <Domternal.BubbleMenu> with the :contexts prop. The component automatically discovers items with bubbleMenu: 'image':
<Domternal.BubbleMenu :contexts="{ image: true }" />You can also specify which items to show per context:
<Domternal.BubbleMenu :contexts="{ image: ['imageFloatLeft', 'imageFloatCenter', 'imageFloatRight', '|', 'editImage', 'deleteImage'], }"/>Without a bubble menu UI, use the float and delete commands programmatically:
// Set image floateditor.commands.setImageFloat('left');editor.commands.setImageFloat('center');editor.commands.setImageFloat('none');
// Delete selected imageeditor.commands.deleteImage();Image popover
Section titled “Image popover”The Image extension includes a built-in popover for inserting images. It opens automatically when the toolbar button emits the insertImage event - no additional setup is required. The popover contains:
- URL input - type or paste an image URL, press
Enterto insert - Apply button (checkmark) - insert the URL from the input
- Browse button (image icon) - open a file picker to select a local image
The browse button works regardless of uploadHandler. When an upload handler is set, the selected file is uploaded via the handler. When null, the file is converted to a base64 data URL and inserted directly.
Keyboard navigation within the popover:
| Key | Action |
|---|---|
Enter (in input) | Insert URL and close |
Enter (on button) | Activate button |
Escape | Close popover |
Tab | Move focus forward: input → apply → browse → input |
Shift+Tab (on buttons) | Move focus backward: browse → apply → input |
The popover is positioned below the toolbar button using Floating UI, appended to document.body to escape overflow: hidden containers, and closes when clicking outside.
The insert popover is URL-only. Alt text is set afterward, on the image itself: see below.
Editing alt text
Section titled “Editing alt text”Alt text can be set when you insert an image, but the moment that matters is afterward, when the image is on the page and you can see what to describe. Select an existing image and the bubble menu shows an Edit alt text action (it appears next to the float controls under the image context). It opens a compact popover with a single field, pre-filled with the image’s current alt, anchored to the image rather than to the toolbar:
// "Edit alt text" emits the `editImage` event. The extension opens an// alt-only popover for the selected image and writes the new value back// to the node's `alt` attribute, leaving `src` untouched.editor.commands.setImage({ src: existingSrc, alt: 'Quarterly revenue up 12%' });The menu is alt-only: there is no URL field or re-upload, so annotating an image can’t accidentally change it. The action highlights as active when the selected image already has non-empty alt text, so images still missing a description stand out at a glance. Applying writes the value to the node’s alt attribute and an empty value clears it; the alt you set travels with the image into the serialized HTML as a standard alt attribute, which is what a reader’s assistive technology relies on. The popover reuses the image popover container with its own field, inheriting the same keyboard handling (Enter to apply, Escape to cancel) and outside-click dismissal.
Resizing
Section titled “Resizing”Click an image to select it. When selected, four corner resize handles appear (NW, NE, SW, SE). Drag any handle to resize the image width - height scales automatically to preserve aspect ratio.
Resizing is editing, so a read-only editor hides the handles and refuses the drag: a selected image keeps its stored width.
- Minimum width: 50px
- Maximum width: 100% of the container
- Floated images are capped at 60% width
- Width is stored as the
widthattribute on the node
Paste and drag-and-drop
Section titled “Paste and drag-and-drop”Without uploadHandler
Section titled “Without uploadHandler”When uploadHandler is null (default):
- Paste: Image files from clipboard are converted to base64 data URLs and inserted at the cursor position
- Drop: Image files dropped onto the editor are converted to base64 data URLs and inserted at the drop position
With uploadHandler
Section titled “With uploadHandler”When uploadHandler is provided:
- Paste: Image files are uploaded via the handler. A placeholder decoration appears during upload.
- Drop: Image files are uploaded via the handler. A placeholder decoration appears at the drop position.
In both cases, files are validated against allowedMimeTypes and maxFileSize before processing.
A visual drag overlay (dm-dragover class) appears on the editor when dragging image files over it, providing visual feedback.
XSS protection
Section titled “XSS protection”Image URLs are validated at four levels (defense in depth):
- parseHTML - validates
srcwhen parsing HTML content - renderHTML - re-validates on render (returns empty
srcif invalid) - setImage command - validates before insertion
- Input rule - validates in markdown syntax
Blocked protocols:
| Protocol | Reason |
|---|---|
javascript: | Script execution |
vbscript: | Script execution |
file: | Local file access |
data: | Blocked unless allowBase64 is true AND URL starts with data:image/ |
Allowed:
http://andhttps://URLs- Relative paths (
./images/photo.jpg) - Absolute paths (
/uploads/photo.jpg) - Protocol-relative URLs (
//cdn.example.com/img.png) data:image/URLs (whenallowBase64istrue)
NodeView
Section titled “NodeView”Image uses a custom NodeView that provides:
- Resizable container (
div.dm-image-resizable) with four corner handles - Click-to-select - clicking the image creates a
NodeSelection(necessary for floated images where ProseMirror’sposAtCoordsis unreliable) - Selection outline -
2px solidaccent color border when selected - Placement styling -
data-floatanddata-alignattributes on the container, written by one pass: whichever of the two is'none'is removed rather than written, so the container never claims both placements at once
leafText
Section titled “leafText”The alt attribute is returned as the image’s text representation. This affects editor.getText(), clipboard text, and accessibility.
Styling
Section titled “Styling”Image styles are provided by @domternal/theme in _image.scss:
| Class | Description |
|---|---|
.dm-image-resizable | Image wrapper with relative positioning |
.dm-image-handle | Resize handle (8x8px, accent-colored) |
.dm-image-handle-nw/ne/sw/se | Corner-specific handle positioning |
.dm-image-popover | Fixed-position URL input popover |
.dm-image-popover-fields | Column wrapper for the popover’s input(s) |
.dm-image-popover-input | URL text input (min-width: 14rem) |
.dm-image-popover-alt-input | Alt-text input shown when editing an existing image |
.dm-image-popover-btn | Apply and browse buttons |
.dm-dragover | Editor overlay during image drag-and-drop |
Floated images are capped at max-width: 60%. The popover supports light and dark themes automatically.
Exports
Section titled “Exports”import { Image } from '@domternal/extension-image';import type { ImageOptions, SetImageOptions, ImageFloat, ImageAlign, ImagePlacement,} from '@domternal/extension-image';import { imageUploadPluginKey } from '@domternal/extension-image';| Export | Type | Description |
|---|---|---|
Image | Node extension | The image node extension |
ImageOptions | TypeScript type | Options for Image.configure() |
SetImageOptions | TypeScript type | Options for the setImage command |
ImageFloat | TypeScript type | 'none' | 'left' | 'right' | 'center' |
ImageAlign | TypeScript type | 'none' | 'left' | 'center' | 'right' |
ImagePlacement | TypeScript type | 'float' | 'align', the value of the placement option |
imageUploadPluginKey | PluginKey | ProseMirror plugin key for the upload plugin (useful for accessing upload state) |
JSON representation
Section titled “JSON representation”An image with all attributes:
{ "type": "image", "attrs": { "src": "https://example.com/photo.jpg", "alt": "A scenic view", "title": "Photo by Alice", "width": "400", "height": "300", "loading": "lazy", "crossorigin": null, "float": "left", "align": "none" }}A minimal image (all attributes shown with their defaults):
{ "type": "image", "attrs": { "src": "https://example.com/photo.jpg", "alt": null, "title": null, "width": null, "height": null, "loading": null, "crossorigin": null, "float": "none", "align": "none" }}A document with a centered image between paragraphs:
{ "type": "doc", "content": [ { "type": "paragraph", "content": [{ "type": "text", "text": "Check out this photo:" }] }, { "type": "image", "attrs": { "src": "https://example.com/photo.jpg", "alt": "A scenic view", "title": null, "width": "600", "height": null, "loading": null, "crossorigin": null, "float": "center", "align": "none" } }, { "type": "paragraph", "content": [{ "type": "text", "text": "Beautiful, right?" }] } ]}Source
Section titled “Source”@domternal/extension-image - Image.ts
See also
Section titled “See also”- Making Editor Content Accessible: Alt Text and ARIA - blog post on editable alt text and how to write good alt text
- Table - resizable tables with merge, split, and cell toolbar
- Details - collapsible accordion with summary and content
- Security - XSS protection and content sanitization
- Dropcursor - visual indicator for drag-and-drop targets