Custom actions
Add host actions to the editor selection and right-click menu
Add your own buttons with customActions and observe their lifecycle with onCustomAction.
Usage
import { VideoEditor, type CustomAction } from '@layermetry/media-editor/react';
const cleanUpAudio: CustomAction = {
id: 'clean-up-audio',
label: 'Clean up audio',
placements: ['selection', 'contextMenu'],
when: {
itemTypes: ['audio', 'video'],
selection: 'multiple',
matches: (items) => items.every((item) =>
item.type === 'audio' || item.videoWithAudio === true),
},
async run(ctx) {
if (!ctx.verbs) throw new Error('Editing actions are unavailable.');
if (!await ctx.confirm('Apply an audio treatment to these clips?')) return;
ctx.setProgress('Choosing treatment…');
const response = await fetch('/api/audio/preset', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ids: ctx.items.map((item) => item.id) }),
signal: ctx.signal,
});
if (!response.ok) throw new Error('The audio service could not choose a treatment.');
const answer: unknown = await response.json();
if (typeof answer !== 'object' || answer === null ||
!('presetId' in answer) || typeof answer.presetId !== 'string') {
throw new Error('The audio service returned no presetId.');
}
const result = ctx.verbs.execute('audioApplyPreset', {
ids: ctx.items.map((item) => item.id),
presetId: answer.presetId,
scopeReason: 'explicit',
});
if (!result.ok) throw new Error(result.error ?? 'The audio treatment was refused.');
ctx.notify('Audio treatment applied.', 'success');
console.log('Audio cleanup completed', ctx.actionId);
},
onError(error, ctx) {
ctx.notify(error instanceof Error ? error.message : 'Audio cleanup failed.', 'error');
return 'handled';
},
};
export default function App() {
return <div style={{ position: 'fixed', inset: 0 }}>
<VideoEditor licenseKey="YOUR_LICENSE_KEY" onClose={() => undefined}
customActions={[cleanUpAudio]}
onCustomAction={(event) => console.log(event.actionId, event.status)} />
</div>;
}Implement POST /api/audio/preset on your server, returning JSON such as { "presetId": "clean-up-noise" }. Serve production requests over HTTPS. The preset must be one accepted by audioApplyPreset. Add at least two audio clips or videos with sound, select them together, then press Clean up audio. This example keeps provider logic on your server.
Video edits through ctx.verbs must name their target ids and pass scopeReason: 'explicit'. Validation and locks apply. The default undo: 'one-step' groups only editor changes, not host requests or analytics.
Action fields
Each action needs a unique id, a label and a run function.
| Field | Type | Default | Description |
|---|---|---|---|
id | string | Required | Names the action in events and as custom:<id> in itemActions. |
label | string | Required | Sets the button label, tooltip and accessible name. |
run | (ctx) => void or Promise<void> | Required | Runs your workflow when the user presses the button. |
icon | CustomActionIcon | None | Uses { svg } or { url } icon data; React components are not accepted here. |
variant | 'button' or 'icon' | 'button' | Chooses words with the icon or an icon-only selection button. |
placements | Array of 'selection' or 'contextMenu' | ['selection'] | Chooses the selection controls, right-click menu, or both. |
layouts | Array of 'regular' or 'compact' | Both | Chooses layouts above 767 px or at most 767 px wide. |
when.itemTypes | Array of item types | Any | Requires every selected item to match a listed type. |
when.selection | 'single', 'multiple' or 'any' | 'single' | Requires one, more than one, or any nonempty selection. |
when.matches | (items) => boolean | None | Adds a synchronous check of the selected items. |
undo | 'one-step' or 'per-verb' | 'one-step' | Groups video edits into one Undo step or keeps one step per editing action. |
priority | number | 0 | Orders custom actions from highest to lowest, preserving list order for ties. |
group | string | None | Names the right-click overflow submenu when its actions all share the same group. |
onError | (error, ctx) => void or 'handled' | None | Handles a thrown or rejected run. |
when.matches runs frequently, including during a drag. Keep it fast and synchronous. It receives shallow frozen item copies. Throwing hides the action and produces one warning. A viewer sees no custom actions. Hiding an action does not grant permission to edit a locked item.
Run context
run receives the selected items as they were when the button was pressed.
| Field | Type | Description |
|---|---|---|
editor | 'video' or 'image' | Identifies the editor. |
actionId | string | Identifies this action. |
items | Read-only item array | Holds deep frozen copies of plain item data; decoded pictures, files and functions are omitted. |
verbs | Editing handle or null | Exposes execute, executeAsync, list and getItems in the video editor; it is null in the image editor. |
signal | AbortSignal | Aborts when the editor closes or the host removes the running action. |
notify | (message, status?) => void | Shows a five-second notice, with status 'info' by default. |
confirm | (message, options?) => Promise<boolean> | Asks a question with Continue and Cancel by default. |
setProgress | (label: string or null) => void | Changes the busy button's text or clears it with null. |
confirm accepts confirmLabel and cancelLabel. A closed editor answers false and shows no further notices. Use signal for host requests. A request that ignores it can continue on your server after the editor closes.
Only one custom action runs at a time in an editor. Other custom buttons are disabled while it runs. There is no automatic run timeout. An action that fails keeps edits it already made; Undo can restore them. The SDK does not roll back your server calls.
Placement and overflow
Custom actions appear after the editor's own controls.
| Surface | ui.customActions.shown field | Default | Maximum |
|---|---|---|---|
| Compact selection bar | compact.selection | 2 | 4 |
| Video desktop floating pill | regular.selection | 2 | 4 |
| Bar above the canvas, desktop | regular.toolbar | 3 | 6 |
| Right-click menu | regular.menu | 5 | 10 |
On a desktop, your actions appear on the bar above the canvas, in both editors. In the video editor the bar shows them for every type of item: video clip, sound clip, picture, sticker, GIF, shape, text, label, caption track and several items. For a video clip, picture, sticker or shape they appear on the bar and on the floating pill. Bar actions use icons when supplied, with their labels as tooltips; actions without an icon show their words. The bar shows the first 3 of them. The rest are in a More actions menu that holds only custom actions.
Remaining actions go into a More actions menu. The right-click menu uses one submenu, named by a common group or More actions. The list accepts at most 24 actions. Extra actions are omitted with a warning. Built-in controls are never displaced.
const ui = {
customActions: {
shown: {
regular: { selection: 2, toolbar: 3, menu: 5 },
compact: { selection: 2 },
},
},
};Counts must be whole numbers of zero or more. Zero puts all custom actions in overflow. A count above the maximum is lowered with a warning. An invalid count uses the default. No custom actions appear when customActions is omitted.
Events and errors
onCustomAction receives started, then one final status.
| Field | Type | Description |
|---|---|---|
actionId | string | Identifies the action. |
status | 'started', 'done', 'failed' or 'aborted' | Reports the run state. |
error | Optional string | Holds the error message for a failed run. |
durationMs | Optional number | Reports elapsed milliseconds on final events. |
A thrown or rejected run calls onError. Returning 'handled' suppresses the default «label» could not finish. notice; the event still reports failed. Removing an action or closing the editor aborts its signal and sends aborted. Video edits requested after closure return ok: false with reason 'editor-closed'.
There is no declarative step-by-step workflow format in 1.0.0. Your run function is the workflow.
Custom element
Set actions through the element's props property; functions cannot be HTML attributes.
<!doctype html>
<html lang="en">
<head><meta charset="utf-8" /><title>Custom editor actions</title></head>
<body>
<media-editor-video id="editor" style="position: fixed; inset: 0"></media-editor-video>
<script type="module">
import '@layermetry/media-editor';
const el = document.getElementById('editor');
el.props = {
licenseKey: 'YOUR_LICENSE_KEY',
onClose: () => el.remove(),
customActions: [{
id: 'inspect-selection', label: 'Inspect selection',
run: (ctx) => ctx.notify(`Selected: ${ctx.items[0].id}`),
}],
};
el.addEventListener('customaction', (event) => {
console.log(event.detail.actionId, event.detail.status);
});
</script>
</body>
</html>Use a module bundler such as Vite to resolve the package import. The customaction event carries the same payload as onCustomAction. See Events.