The canvas menu provides immediate access to frequently-used actions when you select a design element, offering operations like duplicate, delete, layer reordering, and grouping controls directly on the canvas. This guide shows you how to customize these contextual actions to streamline your users’ editing workflow and match your app’s feature priorities.
Explore a complete code sample on GitHub.
Canvas Menu Architecture#

The canvas menu displays horizontally when a design element is selected, offering quick access to editing actions relevant to that element type.
Key Components:
CanvasMenu.Button- Pre-built button implementation with icon and textCanvasMenu.Divider- Visual separator between menu groupsCanvasMenu.Scope- Provides access to the engine, event handler, and selected element
Configuration#
Canvas menu customization is part of the EditorConfiguration, therefore, in order to configure the canvas menu we need to configure the EditorConfiguration. Below you can find the list of available configurations of the canvas menu. To demonstrate the default values, all properties are assigned to their default values unless specified otherwise.
Available configuration properties:
-
scope- Scope of this component. Every new value will trigger recomposition of allScopedPropertys such asvisible,enterTransition,exitTransitionetc. Consider using Composeandroidx.compose.runtime.Stateobjects in the lambdas for granular recompositions over updating the scope, since scope change triggers full recomposition of the component. Ideally, scope should be updated when the parent scope (scope of the parent component) is updated and when you want to observe changes from theEngine. By default, the scope is updated when the parent scope is updated, when selection is changed to a different design block (or unselected), and when the parent of the currently selected design block is changed to a different design block. -
visible- Control canvas menu visibility based on selection state and editor conditions. By default the value is true when a design block is selected, touch interaction is inactive, no sheet is displayed, the selected design block is neitherDesignBlockType.AudionorDesignBlockType.Page, the editor is not in text edit mode, the scene is paused, and the selected design block is visible at the current playback time. -
modifier- Jetpack Compose modifier of this component. By default empty modifier is applied. -
enterTransition- Animation for when the canvas menu appears. Default value is always no enter transition. -
exitTransition- Animation for when the canvas menu disappears. Default value is always no exit transition. -
decoration- Apply custom styling like background, shadows, and positioning relative to the selected element. The default decoration adds background color, applies rounded corners and positions the list of items next to the selected design block. -
listBuilder- Define the complete list of canvas menu items and their order. Items are only displayed whenvisiblereturnstrue. -
itemDecoration- decoration of the items in the canvas menu. Useful when you want to add custom background, foreground, shadow, paddings etc to the items. Prefer using this decoration when you want to apply the same decoration to all the items, otherwise set decoration to individual items. Default value is always no decoration.
The CanvasMenu.Scope (this instance in all lambdas) provides access to the engine, event handler, and editor context. Use this for advanced customization logic and to maintain consistency with the current editor state.
EditorConfiguration.remember { canvasMenu = { CanvasMenu.remember { // Implementation is too large, check the implementation of CanvasMenu.rememberDefaultScope scope = { CanvasMenu.rememberDefaultScope(parentScope = this) } modifier = { Modifier } visible = { val editorState by editorContext.state.collectAsState() remember(this, editorState) { editorState.isTouchActive.not() && editorState.activeSheet == null && editorContext.safeSelection != null && editorContext.selection.type != DesignBlockType.Page && editorContext.selection.type != DesignBlockType.Audio && editorContext.engine.editor.getEditMode() != "Text" && editorContext.isScenePlaying.not() && editorContext.selection.isVisibleAtCurrentPlaybackTime } } enterTransition = { EnterTransition.None } exitTransition = { ExitTransition.None } // Implementation is too large, check the implementation of CanvasMenu.DefaultDecoration decoration = { CanvasMenu.DefaultDecoration(scope = this) { it() } } listBuilder = { CanvasMenu.ListBuilder.remember { /* Add items */ } } // Default value is { it() } itemDecoration = { Box(modifier = Modifier.padding(2.dp)) { it() } } } }}ListBuilder Configuration#
You can configure the items of the canvas menu using two main approaches:
New List Builder#
The simplest approach is to create a new list builder from scratch. In this example, we add both already declared and custom buttons:
CanvasMenu.ListBuilder.remember { add { CanvasMenu.Button.remember { id = { EditorComponentId("my.package.canvasMenu.button.custom") } onClick = {} vectorIcon = null textString = { "Custom Button" } } } add { CanvasMenu.Button.rememberSelectGroup() } if (editorContext.isSelectionInGroup) { add { CanvasMenu.Divider.remember() } } add { CanvasMenu.Button.rememberSendBackward() } add { CanvasMenu.Button.rememberBringForward() } if (editorContext.canSelectionMove) { add { CanvasMenu.Divider.remember() } } add { CanvasMenu.Button.rememberDuplicate() } add { CanvasMenu.Button.rememberDelete() }}All available predefined buttons are listed below.
Modify Existing List Builder#
In case you already have an existing list builder and want to make simple modifications on it (prepend, append, replace, insert or remove an item) without touching the order invoke modify on the ListBuilder.
// Makes sense to use only with builders that are already available and cannot be modified by you directly.val existingListBuilder = CanvasMenu.ListBuilder.remember { add { CanvasMenu.Button.rememberBringForward() } add { CanvasMenu.Button.rememberSendBackward() } add { CanvasMenu.Button.rememberDuplicate() } add { CanvasMenu.Button.rememberDelete() }}existingListBuilder.modify { addFirst { CanvasMenu.Button.remember { id = { EditorComponentId("my.package.canvasMenu.button.first") } vectorIcon = null textString = { "First Button" } onClick = {} } } addLast { CanvasMenu.Button.remember { id = { EditorComponentId("my.package.canvasMenu.button.last") } vectorIcon = null textString = { "Last Button" } onClick = {} } } addAfter(id = CanvasMenu.Button.Id.bringForward) { CanvasMenu.Button.remember { id = { EditorComponentId("my.package.canvasMenu.button.afterBringForward") } vectorIcon = null textString = { "After Bring Forward" } onClick = {} } } addBefore(id = CanvasMenu.Button.Id.sendBackward) { CanvasMenu.Button.remember { id = { EditorComponentId("my.package.canvasMenu.button.beforeSendBackward") } vectorIcon = null textString = { "Before Send Backward" } onClick = {} } } replace(id = CanvasMenu.Button.Id.duplicate) { // Note that it can be replaced with a component that has a different id. CanvasMenu.Button.rememberDuplicate { vectorIcon = { IconPack.Music } } } remove(id = CanvasMenu.Button.Id.delete)}Available modification operations:
addFirst- prepends a new item at the beginning:
addFirst { CanvasMenu.Button.remember { id = { EditorComponentId("my.package.canvasMenu.button.first") } vectorIcon = null textString = { "First Button" } onClick = {} }}addLast- appends a new item at the end:
addLast { CanvasMenu.Button.remember { id = { EditorComponentId("my.package.canvasMenu.button.last") } vectorIcon = null textString = { "Last Button" } onClick = {} }}addAfter- adds a new item right after a specific item:
addAfter(id = CanvasMenu.Button.Id.bringForward) { CanvasMenu.Button.remember { id = { EditorComponentId("my.package.canvasMenu.button.afterBringForward") } vectorIcon = null textString = { "After Bring Forward" } onClick = {} }}addBefore- adds a new item right before a specific item:
addBefore(id = CanvasMenu.Button.Id.sendBackward) { CanvasMenu.Button.remember { id = { EditorComponentId("my.package.canvasMenu.button.beforeSendBackward") } vectorIcon = null textString = { "Before Send Backward" } onClick = {} }}replace- replaces an existing item with a new item:
replace(id = CanvasMenu.Button.Id.duplicate) { // Note that it can be replaced with a component that has a different id. CanvasMenu.Button.rememberDuplicate { vectorIcon = { IconPack.Music } }}remove- removes an existing item:
remove(id = CanvasMenu.Button.Id.delete)CanvasMenu.Item Configuration#
Each CanvasMenu.Item is an EditorComponent. Its id must be unique which is a requirement for proper component management.
You have multiple options for creating canvas menu items, from simple predefined buttons to fully custom implementations.
Use Predefined Buttons#
Start with predefined buttons which are provided as composable functions. All available predefined buttons are listed below.
Create New Buttons#
If our predefined buttons don’t fit your needs you can create your own. To demonstrate the default values, all properties are assigned to their default values unless specified otherwise:
@Composablefun rememberCanvasMenuButton() = CanvasMenu.Button.remember { id = { EditorComponentId("my.package.canvasMenu.button.newButton") } scope = { val parentScope = this as Scope rememberLastValue(parentScope) { if (editorContext.safeSelection == null) lastValue else CanvasMenu.ItemScope(parentScope = parentScope) } } modifier = { Modifier } visible = { true } enterTransition = { EnterTransition.None } exitTransition = { ExitTransition.None } // Default value is { it() } decoration = { Surface(color = MaterialTheme.colorScheme.background) { it() } } onClick = { editorContext.eventHandler.send(EditorEvent.Sheet.Open(SheetType.Volume())) } // Default value is null icon = { Icon( imageVector = IconPack.Music, contentDescription = null, ) } // Default value is null text = { Text( text = "Hello World", ) } enabled = { true }}Required and optional properties:
-
id- the id of the button. Note that it is highly recommended that every uniqueEditorComponenthas a unique id. By default property contains a random value. -
scope- scope of this component. Every new value will trigger recomposition of allScopedPropertys such asvisible,enterTransition,exitTransitionetc. Consider using Composeandroidx.compose.runtime.Stateobjects in the lambdas for granular recompositions over updating the scope, since scope change triggers full recomposition of the component. Ideally, scope should be updated when the parent scope (scope of the parent component) is updated and when you want to observe changes from theEngine. By default the scope is updated only when the parent component scope is updated. -
visible- whether the button should be visible. Default value is always true. -
modifier- Jetpack Compose modifier of this component. By default empty modifier is applied. -
enterTransition- transition of the button when it enters the parent composable. Default value is always no enter transition. -
exitTransition- transition of the button when it exits the parent composable. Default value is always no exit transition. -
decoration- decoration of the button. Useful when you want to add custom background, foreground, shadow, paddings etc. Default value is always no decoration. -
onClick- the callback that is invoked when the button is clicked. By default it is a no-op. -
icon- the icon content of the button. If null, it will not be rendered. Default value is null. -
text- the text content of the button. If null, it will not be rendered. Default value is null. -
tint- the tint color of the content. By default it isMaterialTheme.colorScheme.onSurfaceVariant. -
enabled- whether the button is enabled. Default value is always true.
This gives full control over the content of the button. However, there are simpler configuration options if you do not want to fully customize text and icon composables. Let’s have a look at this example:
@Composablefun rememberCanvasMenuButtonSimple() = CanvasMenu.Button.remember { id = { EditorComponentId("my.package.canvasMenu.button.newButton") } scope = { val parentScope = this as Scope rememberLastValue(parentScope) { if (editorContext.safeSelection == null) lastValue else CanvasMenu.ItemScope(parentScope = parentScope) } } modifier = { Modifier } visible = { true } enterTransition = { EnterTransition.None } exitTransition = { ExitTransition.None } // Default value is it decoration = { Surface(color = MaterialTheme.colorScheme.background) { it() } } onClick = { editorContext.eventHandler.send(ShowLoading) } // Default value is null vectorIcon = { IconPack.Music } // Default value is null text = { "Hello World" } tint = { MaterialTheme.colorScheme.onSurfaceVariant } enabled = { true } contentDescription = null}It has three differences:
iconis replaced withvectorIconlambda, that returnsImageVectorinstead of drawing the icon content.textis replaced withtextStringlambda, that returnsStringinstead of drawing the text content.contentDescriptionproperty is added that is used by accessibility services to describe what the button does. Provide it whenever the button does not contain visible text explaining its action.
Create Dividers#
Use dividers to visually separate groups of related actions:
@Composablefun rememberCanvasMenuDivider() = CanvasMenu.Divider.remember { scope = { val parentScope = this as Scope rememberLastValue(parentScope) { if (editorContext.safeSelection == null) lastValue else CanvasMenu.ItemScope(parentScope = parentScope) } } modifier = { Modifier } visible = { true } enterTransition = { EnterTransition.None } exitTransition = { ExitTransition.None } decoration = { it() } modifier = { remember(this) { Modifier .padding(horizontal = 8.dp) .size(width = 1.dp, height = 24.dp) } }}Required and optional properties:
-
scope- scope of this component. Every new value will trigger recomposition of allScopedPropertys such asvisible,enterTransition,exitTransitionetc. Consider using Composeandroidx.compose.runtime.Stateobjects in the lambdas for granular recompositions over updating the scope, since scope change triggers full recomposition of the component. Ideally, scope should be updated when the parent scope (scope of the parent component) is updated and when you want to observe changes from theEngine. By default the scope is updated only when the parent component scope is updated. -
visible- whether the divider should be visible. Default value is always true. -
modifier- Jetpack Compose modifier of this component. By default padding and size modifiers are applied to the divider. -
enterTransition- transition of the divider when it enters the parent composable. Default value is always no enter transition. -
exitTransition- transition of the divider when it exits the parent composable. Default value is always no exit transition. -
decoration- decoration of the divider. Useful when you want to add custom background, foreground, shadow, paddings etc. Default value is always no decoration.
Create Custom Items#
For completely custom implementations, use EditorComponent.remember and render your custom UI inside decoration. To demonstrate the default values, all properties are assigned to their default values unless specified otherwise:
@Composablefun rememberCanvasMenuCustomItem() = EditorComponent.remember { id = { EditorComponentId("my.package.canvasMenu.newCustomItem") } scope = { val parentScope = this as Scope rememberLastValue(parentScope) { if (editorContext.safeSelection == null) lastValue else CanvasMenu.ItemScope(parentScope = parentScope) } } modifier = { Modifier } visible = { true } enterTransition = { EnterTransition.None } exitTransition = { ExitTransition.None } decoration = { Box( modifier = Modifier .fillMaxHeight() .clickable { Toast .makeText(editorContext.activity, "Hello World Clicked!", Toast.LENGTH_SHORT) .show() }, ) { Text( modifier = Modifier.align(Alignment.Center), text = "Hello World", ) } }}Required and optional properties:
-
id- the unique id of the custom item. Note that it is highly recommended that every uniqueEditorComponenthas a unique id. By default it contains a random value. -
scope- the scope of this component. Every new value will trigger recomposition of allScopedPropertys such asvisible,enterTransition, andexitTransition. Consider using ComposeStateobjects in the lambdas for granular recompositions over updating the scope, since scope change triggers full recomposition of the component. Ideally, scope should be updated when the parent scope (scope of the parent componentCanvasMenu-CanvasMenu.Scope) is updated and when you want to observe changes from theEngine. By default it is derived from the parent component scope. -
visible- whether the custom item should be visible. Default value is always true. -
enterTransition- transition of the custom item when it enters the parent composable. Default value is always no enter transition. -
exitTransition- transition of the custom item when it exits the parent composable. Default value is always no exit transition. -
decoration- render your custom item here. You are responsible for drawing the UI, handling clicks, and applying any custom styling. Default value is always no decoration.
List of Available CanvasMenu.Buttons#
All predefined buttons are available as composable functions in the CanvasMenu.Button namespace. Each function returns a CanvasMenu.Button with default properties that you can customize as shown in the Create New Buttons section.
| Button | ID | Description |
|---|---|---|
CanvasMenu.Button.rememberBringForward | CanvasMenu.Button.Id.bringForward | Brings forward currently selected design block via EditorEvent.Selection.BringForward. |
CanvasMenu.Button.rememberSendBackward | CanvasMenu.Button.Id.sendBackward | Sends backward currently selected design block via EditorEvent.Selection.SendBackward. |
CanvasMenu.Button.rememberDuplicate | CanvasMenu.Button.Id.duplicate | Duplicates currently selected design block via EditorEvent.Selection.Duplicate. |
CanvasMenu.Button.rememberDelete | CanvasMenu.Button.Id.delete | Deletes currently selected design block via EditorEvent.Selection.Delete. |
CanvasMenu.Button.rememberSelectGroup | CanvasMenu.Button.Id.selectGroup | Selects the group design block that contains the currently selected design block via EditorEvent.Selection.SelectGroup. |