Keyboard, Mouse, Trackpad, and Stylus Support in Compose

Android apps increasingly run in environments where touch is only one of several ways to interact. A user might type on a Chromebook keyboard, scroll with a trackpad on a tablet, click with a mouse on a desktop-style window, or write with a stylus. In Compose, the most durable way to support that variety is not to build four versions of every interaction. It is to start with semantic controls, make focus intentional, and reach for raw pointer data only when the feature genuinely needs it.

This article shows the practical boundary: what Compose components already handle, where to add keyboard behavior, and when advanced pointer or stylus APIs are appropriate.

Start with the highest-level interaction API

Compose’s pointer input guidance recommends choosing the highest-level abstraction that meets the need. That gives the platform enough semantic information to support more input methods correctly.

For example, use a Material Button, IconButton, ListItem, or clickable for an ordinary action. clickable is more than a tap detector: it brings semantics, visual feedback, hover handling, focus behavior, and keyboard activation support. A raw pointerInput gesture detector does not automatically provide those things.

ListItem(
    headlineContent = { Text(note.title) },
    modifier = Modifier.clickable { onOpenNote(note.id) },
)

That one interaction can work for touch, mouse clicks, keyboard focus and activation, and accessibility services. It also keeps the UI’s meaning clear to assistive technology.

Use low-level pointer handling for a custom interaction that cannot be expressed by a component or a gesture modifier—for example, a multi-pointer canvas, a bespoke selection surface, or a drawing tool. Do not use it just to make a normal row clickable.

Define the expected behavior for each input method

The same screen should not force every input method into a touch-only workflow. Before adding code, decide what a person should be able to do.

InputGood default expectations
Hardware keyboardMove through controls predictably, see focus, activate controls, and use familiar editing shortcuts in text fields.
MouseClick targets, receive hover feedback when it adds value, and use scroll wheels naturally.
TrackpadScroll and navigate standard scrollable content without custom gesture code.
StylusTap ordinary controls and enter text normally; add pressure, tilt, hover, or palm-aware behavior only to features such as a drawing surface.

The key principle is parity: an action that is available by hover alone or by an unlabelled pointer gesture can be unreachable to a keyboard or screen-reader user. Keep the primary action visible and operable through a semantic control.

Let text fields keep their standard keyboard behavior

Compose delegates common editing commands in editable TextFields to the system. That includes familiar selection, copy, paste, undo, redo, and word-navigation shortcuts. Scrollable containers such as LazyColumn, verticalScroll, and scrollable also receive the platform’s expected Page Up and Page Down behavior. See the official keyboard command documentation for the supported defaults.

Avoid intercepting those commands globally. A shortcut that is convenient at the screen level can become frustrating if it steals Ctrl+C, Ctrl+Z, Tab, or arrow keys while the user is editing text.

For software-keyboard actions such as Next, Done, and Search, pair hardware-keyboard support with the relevant IME action and keyboard handling pattern. The two input paths should lead to the same meaningful outcome.

Add shortcuts at the right scope

When a screen owns a non-editing shortcut, handle it on a focusable ancestor or the specific control that owns the action. onPreviewKeyEvent is useful when the parent needs the event before a focused child consumes it.

import androidx.compose.foundation.focusable
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.isCtrlPressed
import androidx.compose.ui.input.key.onPreviewKeyEvent

val searchShortcut = Modifier
    .focusable()
    .onPreviewKeyEvent { event ->
        val isSearchShortcut =
            event.type == KeyEventType.KeyUp &&
                event.isCtrlPressed &&
                event.key == Key.K

        if (isSearchShortcut) {
            onOpenSearch()
            true // This screen handled Ctrl+K.
        } else {
            false // Let Compose and child controls handle everything else.
        }
    }

Consume an event only when the UI actually owns it. Returning false preserves normal propagation, which is especially important around text fields. Also choose shortcuts that make sense for your audience and document them in the UI when they are essential to a workflow.

Treat focus as a visible navigation system

Keyboard support depends on a predictable focus path, not only on key listeners. Interactive components participate in focus naturally, while custom surfaces can opt in with Modifier.focusable(). Make focus visible enough to understand where Enter, Space, arrows, or an action key will apply.

For a form or complex screen, explicitly control unusual paths with focusProperties, and use FocusRequester in response to a user event—not as a side effect during composition. The Compose focus APIs and their limitations are covered in the official change focus behavior guide.

For field-by-field examples, including Tab, Shift+Tab, D-pad, and directional focus behavior, see Focus Management and Keyboard Navigation in Compose Forms. If you need to refine an unusual focus order, that article is the right companion; this one keeps the broader multi-input design in view.

Design mouse and trackpad interactions as enhancements

Most mouse and trackpad support comes for free when the screen uses normal Compose building blocks:

  • clickable and Material components supply semantic click behavior, focus, and hover-aware feedback.
  • LazyColumn, LazyRow, and scroll modifiers handle ordinary scrolling.
  • A pointer can be a finger, mouse, stylus, or trackpad-related input, so custom pointer code must not quietly assume every pointer is a finger.

Hover is a useful enhancement for revealing a subtle affordance or confirming which dense item is under the pointer. It must not be the only way to discover an action. Compose hit testing treats mouse and stylus hover differently from pressed pointers: the target can update as the pointer moves. That is another reason to keep hover state small, reversible, and separate from the actual action.

When building a custom surface, inspect PointerEvent only after confirming a high-level modifier cannot express the interaction. The Compose gesture guide explains event passes, hit testing, and PointerType in more depth. For ordinary tap, long-press, drag, and swipe interactions, use the established patterns in Click, Long-Press, Drag, and Swipe Gestures in Compose.

Use the stylus as ordinary input until it needs to be special

A stylus can operate ordinary buttons, lists, and text fields through the same semantic UI that works for touch. This is the correct default for productivity screens: you do not need a stylus-only implementation just to support a stylus.

Specialized experiences—handwriting, ink, sketching, annotation, or signature capture—may need data such as tool type, pressure, orientation, tilt, hover, and palm interaction. Android’s stylus input documentation and large-screen input compatibility guide describe this advanced path.

For a drawing canvas that needs Android MotionEvent data, bridge only that canvas with pointerInteropFilter:

import android.view.MotionEvent
import androidx.compose.foundation.Canvas
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.input.pointer.pointerInteropFilter

@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun SketchSurface(
    onMotionEvent: (MotionEvent) -> Boolean,
    modifier: Modifier = Modifier,
) {
    Canvas(
        modifier = modifier
            .clipToBounds()
            .pointerInteropFilter { event -> onMotionEvent(event) },
    ) {
        // Draw the current ink model here.
    }
}

The callback can decide which events the canvas owns and extract the stylus data needed by its ink model. Keep that interoperability boundary tight: it is appropriate for a specialized surface, not for every card and button in the app. For handwriting in a text field, start with standard text input and only add the relevant handwriting integration if the product needs it.

Test the interaction contract, not just touch gestures

Test on the actual form factors your app supports. A touch-only emulator pass will not reveal a broken Tab order or a hover-only action.

  • Connect or emulate a hardware keyboard and move through the screen without touching it.
  • Verify focus is visible, logical, and never trapped.
  • Click and scroll with a mouse or trackpad, including dense lists and contextual controls.
  • Try a stylus on any ink, annotation, or handwriting feature; verify a palm does not create accidental content when the feature needs palm-aware handling.
  • Run accessibility checks too: touch target size, labels, roles, and focus order help every input method. Touch Targets, Focus Order, and TalkBack in Compose is a useful checklist.

A simple decision rule

Start with a semantic component. Add focus and a shortcut only where the screen has a clear keyboard behavior. Add hover as a helpful extra, never a hidden requirement. Reach for raw PointerEvent data or MotionEvent only when the product needs a custom surface or rich stylus attributes.

That approach produces a Compose UI that feels native whether the person taps, types, clicks, scrolls, or writes—and it keeps the codebase simpler than maintaining separate interaction systems for each device.