This HTML page is not optimized for LLM or AI agent consumption. Fetch the Markdown version instead: /guides/android/customizing-the-interface/customizing-the-toolbar.md — it contains the complete documentation content in clean, structured Markdown without any CSS, JavaScript, or navigation noise. Customize contextual toolbars in Android PDF viewer | Nutrient SDK

PdfActivity and PdfUiFragment show a contextual toolbar whenever the user enters a special mode: annotation creation, document editing, or content editing. All three are rendered in Jetpack Compose by one generic bar, and all three are customized through one object, the ToolbarCoordinator. You never construct a toolbar yourself. You observe the coordinator to learn which bar is shown, transform its items, listen to its actions, and tell it where the bar may dock.

This guide covers those customizations for an SDK-hosted integration. For the main toolbar, see customizing the main toolbar. To render the bar yourself in a Compose layout, see the Compose contextual toolbar component page. If you’re coming from a version before 12.0, the 12 migration guide maps the removed View toolbar APIs onto the ones below.

Getting the coordinator

Each host has its own coordinator: nutrientToolbarCoordinator() on an activity returns the activity’s, and on a fragment returns the fragment’s. A PdfUiFragment uses its own, so ask the fragment rather than its activity. The coordinator outlives configuration changes, so you can get it once in onCreate and keep it:

import com.pspdfkit.jetpack.compose.toolbar.nutrientToolbarCoordinator
class MyActivity : PdfActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val coordinator = nutrientToolbarCoordinator()
}
}
// In a `Fragment` host, the same extension exists on `Fragment`:
val coordinator = pdfUiFragment.nutrientToolbarCoordinator()

A Compose host collects it from the document state instead; it’s null until the UI is ready:

val coordinator by documentState.toolbarCoordinator.collectAsState()

The coordinator is Kotlin-first. Every add… method returns a ToolbarSubscription. It’s AutoCloseable, so close it in onDestroy. Java callers pass lambdas that return kotlin.Unit.INSTANCE.

Knowing which bar is shown

The coordinator publishes the shown bar as a ContextualToolbarState, or null when none is shown. The state carries the bar’s kind (Annotation, DocumentEditing, or ContentEditing), its items, its dock position, whether it isDraggable, and its allowedPositions.

val subscription = coordinator.addContextualToolbarChangeListener { state ->
when (state?.kind) {
ContextualToolbarKind.Annotation -> showAnnotationHelp()
ContextualToolbarKind.DocumentEditing -> Unit
ContextualToolbarKind.ContentEditing -> Unit
null -> hideAnnotationHelp() // no contextual toolbar is onscreen.
}
}
override fun onDestroy() {
super.onDestroy()
subscription.close()
}

The listener runs once on registration with the current state, and then on every change. A coroutine host can collect coordinator.contextualToolbar — a StateFlow<ContextualToolbarState?> — instead.

The SDK rebuilds a bar’s items every time it enters the mode, so apply item customizations inside this listener and guard against running them twice. The examples below follow that pattern.

Adding, removing, and changing items

Items are immutable ToolbarItems. Transform the shown bar’s list with updateContextualItems; the transform receives the current list and returns the new one.

Add a custom item

coordinator.addContextualToolbarChangeListener { state ->
if (state?.kind != ContextualToolbarKind.Annotation) return@addContextualToolbarChangeListener
if (state.items.any { it.id == R.id.my_bookmark }) return@addContextualToolbarChangeListener
coordinator.updateContextualItems { items ->
items.add(
ToolbarItem.Button(
actionId = "my.app.bookmark", // stable ID, survives process death.
id = R.id.my_bookmark, // `R.id`, used by click handling and test tags.
contentDescription = "Bookmark", // accessibility label and tooltip.
icon = ToolbarIcon.Resource(R.drawable.ic_bookmark),
position = ToolbarItem.ItemEdge.Start,
),
)
}
}

On the annotation and document editing bars, adding the item isn’t enough on its own. Those bars lay their items out from a grouping preset, and an item the preset doesn’t list isn’t shown. Add your item to the preset as well. Appending it to defaultPreset keeps the stock grouping, and the bar moves the last entries into its More group when they don’t fit:

coordinator.setContextualToolbarGroupingProvider { kind, _, _, defaultPreset ->
if (kind == ContextualToolbarKind.Annotation) {
defaultPreset + ToolbarPresetEntry(R.id.my_bookmark, "my.app.bookmark")
} else {
defaultPreset
}
}

The content editing bar has no preset and shows every item in its list.

Use a ToolbarItem.Toggle for an item with a selected state, such as a custom tool. Keep state.isSelected in sync with the mode from an OnAnnotatingModeChangeListener. ToolbarItem.Submenu groups children behind one button.

Remove an item

coordinator.updateContextualItems { items ->
items.removeAll { it.id == R.id.pspdf__annotation_toolbar_item_image }
}

This is also how you hide the stylus toggle. It appears automatically whenever a stylus is connected; filter out R.id.pspdf__annotation_toolbar_item_stylus to remove it.

Replace an icon or change state

Return a copy of the item rather than mutating it:

coordinator.updateContextualItems { items ->
items.map { item ->
when (item.id) {
R.id.pspdf__annotation_toolbar_item_note ->
(item as ToolbarItem.Toggle).copy(icon = ToolbarIcon.Resource(R.drawable.my_note_icon))
R.id.pspdf__annotation_toolbar_item_image -> item.disabled()
else -> item
}
}.toPersistentList()
}
// `ToolbarItem` is a sealed interface; each variant has its own `copy()`.
private fun ToolbarItem.disabled(): ToolbarItem = when (this) {
is ToolbarItem.Button -> copy(state = state.copy(isEnabled = false))
is ToolbarItem.Toggle -> copy(state = state.copy(isEnabled = false))
is ToolbarItem.Submenu -> copy(state = state.copy(isEnabled = false))
}

To restrict which annotation tools appear at all, prefer the configuration: PdfActivityConfiguration.Builder.enabledAnnotationTools() removes tools from the bar and from every other entry point at once. See annotation creation.

Handling clicks

Actions from every toolbar arrive on one bus. An ItemClicked action carries the item’s id and the kind of the bar it came from, or null for the main toolbar. You can then tell an annotation item from a main toolbar item with the same ID. A tap on an item inside an open submenu arrives as SubmenuChildClicked with the group’s id and the childId.

val actions = coordinator.addActionListener { action ->
when (action) {
is ToolbarAction.ItemClicked ->
if (action.kind == ContextualToolbarKind.Annotation && action.id == R.id.my_bookmark) addBookmark()
is ToolbarAction.SubmenuChildClicked ->
if (action.childId == R.id.my_bookmark) addBookmark()
else -> Unit
}
}

addActionListener observes. The SDK’s own handler runs as well, so a listener on a built-in item sees the tap but can’t stop the tool from activating. To consume a click, register an interceptor. It runs before every listener, and returning true stops the SDK from seeing the action:

coordinator.addActionInterceptor { action ->
action is ToolbarAction.ItemClicked &&
action.kind == ContextualToolbarKind.Annotation &&
action.id == R.id.pspdf__annotation_toolbar_item_image &&
!userMayInsertImages() // `true` = consumed, the image tool never activates.
}

Custom tool items

A custom item that should start an annotation tool enters the mode itself from the action listener. Pair it with a Toggle whose selection follows the active tool:

coordinator.addActionListener { action ->
if (action is ToolbarAction.ItemClicked && action.id == R.id.line_blue) {
val variant = AnnotationToolVariant.fromName("line_blue")
val controller = annotatingController
if (controller?.activeAnnotationToolVariant == variant) {
pdfFragment?.exitCurrentlyActiveMode()
} else {
pdfFragment?.enterAnnotatingMode(AnnotationTool.LINE, variant)
}
}
}

Configure the variant’s defaults through the fragment’s annotationConfiguration, as shown in AnnotationToolbarCustomItemsExample in the Catalog app. Long presses aren’t exposed on the action bus, because a long press opens a group’s submenu.

Grouping items

When a bar has more items than slots, it groups them into submenus. Which items group together, and in which order they appear, is a preset that depends on the bar’s capacity. Provide your own with a ContextualToolbarGroupingProvider:

coordinator.setContextualToolbarGroupingProvider { kind, capacity, itemCount, defaultPreset ->
if (kind != ContextualToolbarKind.Annotation) return@setContextualToolbarGroupingProvider defaultPreset
when {
capacity < 4 -> defaultPreset
capacity <= 7 -> listOf(
ToolbarPresetEntry(R.id.my_bookmark, "my.app.bookmark"),
ToolbarPresetEntry(
id = R.id.pspdf__annotation_toolbar_group_markup,
actionId = "my.app.group.markup",
childIds = listOf(
R.id.pspdf__annotation_toolbar_item_highlight,
R.id.pspdf__annotation_toolbar_item_squiggly,
R.id.pspdf__annotation_toolbar_item_strikeout,
R.id.pspdf__annotation_toolbar_item_underline,
),
),
ToolbarPresetEntry(R.id.pspdf__annotation_toolbar_item_picker, "pspdf.annotation.picker"),
ToolbarPresetEntry(R.id.pspdf__annotation_toolbar_item_image, "pspdf.annotation.image"),
)
else -> listOf(
ToolbarPresetEntry(R.id.my_bookmark, "my.app.bookmark"),
ToolbarPresetEntry(R.id.pspdf__annotation_toolbar_item_highlight, "pspdf.annotation.highlight"),
ToolbarPresetEntry(R.id.pspdf__annotation_toolbar_item_squiggly, "pspdf.annotation.squiggly"),
ToolbarPresetEntry(R.id.pspdf__annotation_toolbar_item_strikeout, "pspdf.annotation.strikeout"),
ToolbarPresetEntry(R.id.pspdf__annotation_toolbar_item_underline, "pspdf.annotation.underline"),
ToolbarPresetEntry(R.id.pspdf__annotation_toolbar_item_picker, "pspdf.annotation.picker"),
ToolbarPresetEntry(R.id.pspdf__annotation_toolbar_item_image, "pspdf.annotation.image"),
)
}
}

How the provider is called:

  • It runs on every measure of the bar. It receives the bar’s kind, capacity, flat itemCount, and the SDK’s defaultPreset for that capacity. The capacity counts item slots along the bar’s main axis: width at the top dock, height at a side dock. Return defaultPreset to keep the stock grouping for a kind you don’t customize.
  • An entry with childIds becomes a submenu. Use one of the IDs marked group — such as pspdf__annotation_toolbar_group_markup or pspdf__annotation_toolbar_group_drawing — and never an item ID. Give it a namespaced actionId. pspdf.* is reserved for SDK items.
  • The bar skips IDs missing from the flat item list, so a preset may name items that configuration or licensing disables.
  • Include your own items in the preset. On the annotation and document editing bars, an item the preset doesn’t list isn’t shown.
  • An annotation group button never has its own icon; it shows the last selected child. Other bars give some groups a fixed icon — for example, pspdf__document_editing_toolbar_group_more shows an ellipsis.

The provider isn’t persisted. Register it in onCreate, before any bar is shown; it applies to the shown bar immediately when registered later. AnnotationToolbarGroupingExample in the Catalog app registers a preset this way. In AnnotationToolbarUserEditableExample, the user arranges the tools by drag and drop and the app saves the arrangement.

Subclasses of the deprecated PresetMenuItemGroupingRule keep working for one major version through coordinator.setMenuItemGroupingRule(context, kind, rule); the 12 migration guide explains the bridge and its limits.

Docking the toolbar

A contextual toolbar docks at the top or at the start or end edge of the window, and the user can drag it between docks. Positions are logical: ToolbarPosition.Start is on the left in a left-to-right layout and on the right in a right-to-left one.

coordinator.addContextualToolbarChangeListener { state ->
if (state == null) return@addContextualToolbarChangeListener
coordinator.setContextualToolbarPosition(ToolbarPosition.End) // move the shown bar.
coordinator.setContextualToolbarDraggable(false) // hide the drag handle and drop zones.
}

Both setters act on the currently shown bar and are silent no-ops while none is shown, so call them from the change listener.

To limit where the user may dock any bar, restrict the allowed positions once. The host can only narrow a bar’s own set. A bar on a dock that is no longer allowed moves to the first allowed one. Passing null clears the restriction:

coordinator.setContextualToolbarAllowedPositions(listOf(ToolbarPosition.Top))

The user’s last dock is persisted per toolbar ID, app-wide, so a drag inside one activity is visible to every host in the app. Read or preset it through PSPDFKitPreferences:

PSPDFKitPreferences.get(context)
.setLastToolbarPosition(R.id.pspdf__annotation_toolbar, ToolbarDockPosition.LEFT)

ToolbarDockPosition is the physical dock (TOP, LEFT, RIGHT) that the preference stores.

Behavior to know about

  • The system bars stay while a contextual toolbar is shown. Entering annotation creation mode with a side-docked bar hides the rest of the user interface when hideUserInterfaceWhenCreatingAnnotations is on. The status and navigation bars remain, so the bar the user is working in never sits alone on a fullscreen page.
  • The document doesn’t move when a bar appears or changes dock. The content inset depends only on the SDK’s main toolbar.
  • Long-pressing an icon-only item shows a tooltip with its contentDescription, on the bar’s document side.
  • UI tests address items by Compose test tag carrying the resource entry name — for example, onNodeWithTag("pspdf__annotation_toolbar_item_undo"). The bar’s surface is tagged pspdf__contextual_toolbar. The bars have no View IDs.

Theming

The bars read the pspdf__contextualToolbarStyle theme attribute, whose style takes the pspdf__ContextualToolbar attributes: pspdf__backgroundColor, pspdf__borderColor, pspdf__iconsColor, pspdf__iconsColorActivated, pspdf__submenuBackgroundColor, and pspdf__submenuBorderColor. The drag target color and the bar height come from pspdf__toolbarCoordinatorLayoutStyle. The bars expose no per-item icon attributes, so replace an icon through updateContextualItems as shown above. See appearance styling for the full list, and the Compose contextual toolbar page for programmatic colors when you render the bar yourself.

Working examples

These examples are in the Catalog app under Toolbars & Menus:

  • AnnotationToolbarGroupingExample — A grouping preset and a custom item on the annotation toolbar.
  • AnnotationToolbarCustomItemsExample — Custom toggle items bound to tool variants, with selection kept in sync.
  • AnnotationToolbarUserEditableExample — The user arranges the tools with drag and drop; the arrangement is saved.
  • CustomToolbarsPdfActivityExample — The SDK bars disabled and the host’s own Compose bars on the same coordinator.