The navigation bar serves as the primary control interface at the top of the editor, housing essential functions like session management (close/save), editing operations (undo/redo), mode switching, and export capabilities. This guide shows you how to customize the navigation layout, button placement, and functionality to align with your app’s information architecture and user flow patterns.
Explore a complete code sample on GitHub.
Navigation Bar Architecture#

The navigation bar displays horizontally at the top of the editor, with items organized into alignment groups: start (left), center, and end (right).
Key Components:
NavigationBar.Button- Pre-built button implementation with icon and textNavigationBar.Scope- Provides access to the engine, event handler, and editor state
Configuration#
Navigation bar customization is part of the EditorConfiguration, therefore, in order to configure the navigation bar we need to configure the EditorConfiguration. Below you can find the list of available configurations of the navigation bar. To demonstrate the default values, all properties are assigned to their default values.
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 only when the parent scope is updated. -
visible- Control navigation bar visibility. Default value is always true. -
modifier- Jetpack Compose modifier of this component. By default empty modifier is applied. -
enterTransition- Animation for when the navigation bar appears. Default value is always no enter transition. -
exitTransition- Animation for when the navigation bar disappears. Default value is always no exit transition. -
decoration- Apply custom styling like background, shadows, and padding. By defaultNavigationBar.DefaultDecorationadds a surface-colored background and horizontal padding. -
listBuilder- Define the complete list of navigation bar items and their alignment groups. Items are only displayed whenvisiblereturnstrue. By default,listBuilderdoes not add anything to the navigation bar. -
horizontalArrangement- Layout arrangement for navigation bar items. Controls spacing and alignment of items within the navigation bar. Default value isArrangement.Start. -
itemDecoration- decoration of the items in the navigation bar. 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 NavigationBar.Scope (this instance in all lambdas) provides access to the engine, event handler, and editor state. Use this for advanced customization logic and to maintain consistency with the current editor state.
EditorConfiguration.remember { navigationBar = { NavigationBar.remember { scope = { remember(this) { NavigationBar.Scope(parentScope = this) } } visible = { true } modifier = { Modifier } enterTransition = { EnterTransition.None } exitTransition = { ExitTransition.None } decoration = { // Also available via NavigationBar.DefaultDecoration Box( modifier = Modifier .fillMaxWidth() .heightIn(min = 64.dp) .background(MaterialTheme.colorScheme.surface) .padding(PaddingValues(horizontal = 4.dp)), contentAlignment = Alignment.Center, ) { it() } } listBuilder = { NavigationBar.ListBuilder.remember { /* Add items */ } } horizontalArrangement = { Arrangement.SpaceEvenly } // Default value is { it() } itemDecoration = { Box(modifier = Modifier.padding(2.dp)) { it() } } } }}ListBuilder Configuration#
You can configure the items of the navigation bar using two main approaches:
New List Builder#
The simplest approach is to create a new list builder from scratch. Since navigation bar contains aligned groups most of the time (some items in the center, some in the left, some in the right), we are going to use aligned keyword in the list builder. In this example, we add both already declared and custom buttons:
NavigationBar.ListBuilder.remember { aligned(alignment = Alignment.Start) { add { NavigationBar.Button.rememberCloseEditor() } } aligned(alignment = Alignment.CenterHorizontally) { add { NavigationBar.Button.remember { id = { EditorComponentId("my.package.navigationBar.button.custom") } vectorIcon = null textString = { "Custom Button" } onClick = {} } } } aligned( alignment = Alignment.End, arrangement = Arrangement.spacedBy(2.dp), ) { add { NavigationBar.Button.rememberExport() } add { NavigationBar.Button.rememberUndo() } add { NavigationBar.Button.rememberRedo() } }}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 = NavigationBar.ListBuilder.remember { aligned(alignment = Alignment.End) { aligned(alignment = Alignment.Start) { add { NavigationBar.Button.rememberCloseEditor() } } aligned(alignment = Alignment.End) { add { NavigationBar.Button.rememberUndo() } add { NavigationBar.Button.rememberRedo() } add { NavigationBar.Button.rememberTogglePagesMode() } add { NavigationBar.Button.rememberExport() } } }}existingListBuilder.modify { addFirst(alignment = Alignment.End) { NavigationBar.Button.remember { id = { EditorComponentId("my.package.navigationBar.button.endAligned.first") } vectorIcon = { IconPack.Music } textString = { "First Button" } onClick = {} } } addLast(alignment = Alignment.End) { NavigationBar.Button.remember { id = { EditorComponentId("my.package.navigationBar.button.endAligned.last") } vectorIcon = { IconPack.Music } textString = { "Last Button" } onClick = {} } } addAfter(id = NavigationBar.Button.Id.redo) { NavigationBar.Button.remember { id = { EditorComponentId("my.package.navigationBar.button.afterRedo") } vectorIcon = { IconPack.Music } textString = { "After Redo" } onClick = {} } } addBefore(id = NavigationBar.Button.Id.undo) { NavigationBar.Button.remember { id = { EditorComponentId("my.package.navigationBar.button.beforeUndo") } vectorIcon = { IconPack.Music } textString = { "Before Undo" } onClick = {} } } replace(id = NavigationBar.Button.Id.export) { NavigationBar.Button.remember { id = { EditorComponentId("my.package.navigationBar.button.replacedExport") } vectorIcon = null text = { "Replaced Export" } onClick = {} } } remove(id = NavigationBar.Button.Id.closeEditor)}Available modification operations:
addFirst- prepends a new item at the beginning of an alignment group:
addFirst(alignment = Alignment.End) { NavigationBar.Button.remember { id = { EditorComponentId("my.package.navigationBar.button.endAligned.first") } vectorIcon = { IconPack.Music } textString = { "First Button" } onClick = {} }}addLast- appends a new item at the end of an alignment group:
addLast(alignment = Alignment.End) { NavigationBar.Button.remember { id = { EditorComponentId("my.package.navigationBar.button.endAligned.last") } vectorIcon = { IconPack.Music } textString = { "Last Button" } onClick = {} }}addAfter- adds a new item right after a specific item:
addAfter(id = NavigationBar.Button.Id.redo) { NavigationBar.Button.remember { id = { EditorComponentId("my.package.navigationBar.button.afterRedo") } vectorIcon = { IconPack.Music } textString = { "After Redo" } onClick = {} }}addBefore- adds a new item right before a specific item:
addBefore(id = NavigationBar.Button.Id.undo) { NavigationBar.Button.remember { id = { EditorComponentId("my.package.navigationBar.button.beforeUndo") } vectorIcon = { IconPack.Music } textString = { "Before Undo" } onClick = {} }}replace- replaces an existing item with a new item:
replace(id = NavigationBar.Button.Id.export) { NavigationBar.Button.remember { id = { EditorComponentId("my.package.navigationBar.button.replacedExport") } vectorIcon = null text = { "Replaced Export" } onClick = {} }}remove- removes an existing item:
remove(id = NavigationBar.Button.Id.closeEditor)NavigationBar.Item Configuration#
Each NavigationBar.Item is an EditorComponent. Its id must be unique which is a requirement for proper component management. Items must be organized within alignment groups when using the aligned layout system.
Use Predefined Buttons#
Start with predefined buttons which are provided as composable functions. All available predefined buttons are listed below.
Create New Buttons#
Create custom buttons when predefined options don’t meet your needs:
@Composablefun rememberNavigationBarButton() = NavigationBar.Button.remember { id = { EditorComponentId("my.package.navigationBar.button.newButton") } scope = { remember(this) { NavigationBar.ItemScope(parentScope = this) } } 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. -
modifier- Jetpack Compose modifier of this component. By default empty modifier is applied. -
visible- whether the button should be visible. Default value is always true. -
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 rememberNavigationBarButtonSimple() = NavigationBar.Button.remember { id = { EditorComponentId("my.package.navigationBar.button.newButton") } scope = { remember(this) { NavigationBar.ItemScope(parentScope = this) } } 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.CloseEditor()) } // Default value is null vectorIcon = { IconPack.Music } // Default value is null textString = { "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 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 rememberNavigationBarCustomItem() = EditorComponent.remember { id = { EditorComponentId("my.package.navigationBar.newCustomItem") } scope = { remember(this) { NavigationBar.ItemScope(parentScope = this) } } 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- 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 it is derived from the parent component scope. -
modifier- Jetpack Compose modifier of this component. By default empty modifier is applied. -
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 NavigationBar.Buttons#
All predefined buttons are available as composable functions in the NavigationBar.Button namespace. Each function returns a NavigationBar.Button with default properties that you can customize as shown in the Create New Buttons section.
| Button | Id | Description |
|---|---|---|
NavigationBar.Button.rememberCloseEditor | NavigationBar.Button.Id.closeEditor | Triggers EditorConfiguration.onClose callback via EditorEvent.OnClose. |
NavigationBar.Button.rememberUndo | NavigationBar.Button.Id.undo | Does undo operation in the editor via EditorApi.undo engine API. |
NavigationBar.Button.rememberRedo | NavigationBar.Button.Id.redo | Does redo operation in the editor via EditorApi.redo engine API. |
NavigationBar.Button.rememberExport | NavigationBar.Button.Id.export | Triggers EditorConfiguration.onExport callback via EditorEvent.Export. |
NavigationBar.Button.rememberTogglePreviewMode | NavigationBar.Button.Id.togglePreviewMode | Updates editor view mode via EditorEvent.SetViewMode: when current view mode is EditorViewMode.Edit, then EditorViewMode.Preview is set and vice versa. Note that this button is intended to be used in Photo Editor, Apparel Editor and Postcard Editor and may cause unexpected behaviors when used in other solutions. |
NavigationBar.Button.rememberTogglePagesMode | NavigationBar.Button.Id.togglePagesMode | Updates editor view mode via EditorEvent.SetViewMode: when current view mode is EditorViewMode.Edit, then EditorViewMode.Pages is set and vice versa. Note that this button is intended to be used in Design Editor and may cause unexpected behaviors when used in other solutions. |
NavigationBar.Button.rememberPreviousPage | NavigationBar.Button.Id.previousPage | Navigates to the previous page via EditorEvent.Navigation.ToPreviousPage. |
NavigationBar.Button.rememberNextPage | NavigationBar.Button.Id.nextPage | Navigates to the next page via EditorEvent.Navigation.ToNextPage. |