
Attach native keyboard accessory toolbars and customizable input actions to TextField via modifier; focus-aware lifecycle, SF Symbols icons, flexible spacers, dynamic real-time updates.
English | 한국어
A Kotlin Multiplatform (KMP) library that brings native platform text-input actions and accessory toolbars to Compose Multiplatform TextFields.
On iOS, it attaches native UIToolbar keyboard accessory views directly to the active native text responder owned by Compose, while preserving full Compose ownership of your text state and focus lifecycle. On Android, Desktop (JVM), macOS, and Web (Wasm JS), it gracefully passes through (no-op) without altering standard text editing behavior, ensuring full cross-platform compatibility across your shared Compose code.
TextField using Modifier.inputActions(...).InputAction.FlexibleSpace).InputActionsHost inside commonMain for seamless multiplatform deployment.| Feature | iOS | Android | Desktop (JVM) | macOS | Web (Wasm JS) | Completion Rate | Under the Hood |
|---|---|---|---|---|---|---|---|
| InputActionsHost | 🟢 Yes | 🟢 Yes | 🟢 Yes | 🟢 Yes | 🟢 Yes | 100% | Common CompositionLocal host providing active action registries |
| Modifier.inputActions | 🟢 Yes | 🟢 Yes | 🟢 Yes | 🟢 Yes | 🟢 Yes | 100% | Modifier Node listening to FocusEventModifierNode events |
| Plain & Emphasized Actions | 🟢 Yes | 🟡 No-op | 🟡 No-op | 🟡 No-op | 🟡 No-op | 95% | Native UIBarButtonItem with Plain / Done styles on iOS |
| Icon Actions (SF Symbols) | 🟢 Yes | 🟡 No-op | 🟡 No-op | 🟡 No-op | 🟡 No-op | 95% |
UIImage.systemImageNamed(...) on iOS |
| Flexible Spacers | 🟢 Yes | 🟡 No-op | 🟡 No-op | 🟡 No-op | 🟡 No-op | 100% |
UIBarButtonItem(barButtonSystemItem: .flexibleSpace) on iOS |
| Dynamic Action Updates | 🟢 Yes | 🟢 Yes | 🟢 Yes | 🟢 Yes | 🟢 Yes | 90% | Real-time toolbar item array updates during focus |
Add the dependency to your shared Kotlin Multiplatform module's build.gradle.kts:
kotlin {
sourceSets {
commonMain.dependencies {
implementation("zone.ien.inputactions:inputactions:0.9.0-alpha02")
}
}
}Wrap your Compose UI tree inside your shared commonMain code with InputActionsHost:
// commonMain/kotlin/App.kt
import zone.ien.inputactions.InputActionsHost
@Composable
fun App() {
MaterialTheme {
InputActionsHost {
MainScreen()
}
}
}InputActionsHost intercepts focus events for fields using Modifier.inputActions(...) and attaches the native UIToolbar to the active iOS keyboard responder.InputActionsHost acts as a seamless container (no-op), allowing each platform's standard text-input behavior to operate without additional layout configuration.[!IMPORTANT] Toolchain Requirements: Built and tested with Kotlin 2.4.20-RC2, Compose Multiplatform 1.12.0, and JDK 17. Android targets use minSdk 29 and compileSdk/targetSdk 37.
A wrapper @Composable component that establishes the local action registry host via CompositionLocal.
| Parameter | Type | Default | Description |
|---|---|---|---|
content |
@Composable () -> Unit |
Required | The sub-tree containing TextField components configured with inputActions. |
Modifier extension for Compose text fields to register actions triggered when the field gains focus.
| Parameter | Type | Default | Description |
|---|---|---|---|
vararg actions |
InputAction |
Required | Vararg list of InputAction instances presented in the native keyboard toolbar. |
Represents an action item or spacer displayed in the keyboard accessory toolbar.
class InputAction(
val title: String = "",
val style: InputActionStyle = InputActionStyle.Plain,
val icon: InputActionIcon? = null,
val hidesSharedBackground: Boolean = false,
val separatesSharedBackground: Boolean = false,
val onClick: () -> Unit,
)| Property | Type | Default | Description |
|---|---|---|---|
title |
String |
"" |
Text label displayed on the action button. Can be empty for icon-only actions; ignored for FlexibleSpace. |
style |
InputActionStyle |
InputActionStyle.Plain |
Presentation style (Plain or Done). |
isFlexibleSpace |
Boolean |
false |
Indicates whether this item is a flexible spacer (true) or action button (false). |
icon |
InputActionIcon? |
null |
Optional platform icon. Displays an SF Symbol icon when specified on iOS. |
hidesSharedBackground |
Boolean |
false |
Hides the item's shared toolbar background on iOS 26+. This is different from separating two visible groups. |
separatesSharedBackground |
Boolean |
false |
Inserts UIKit's zero-width fixedSpaceItem() on iOS 26+ so adjacent groups keep their own backgrounds. |
onClick |
() -> Unit |
Required | Callback executed when the action button is tapped. Ignored for FlexibleSpace. |
InputAction.FlexibleSpace: Standard flexible spacer dividing buttons.InputAction.FlexibleSpace(separatesSharedBackground: Boolean = false): Flexible spacer with optional system group separation. This uses UIBarButtonItem.fixedSpaceItem() on iOS 26+.separatesSharedBackground keeps both backgrounds visible and adds a system separator between
two groups. It does not create a custom background, blur, or corner treatment.
InputAction, the separator is placed immediately before that action. Put the
option on the first action of the group that should begin after the boundary.InputAction.FlexibleSpace(separatesSharedBackground = true), the flexible gap remains and
the separator is placed after that gap.FlexibleSpace is supplied and there are multiple actions, the library automatically
inserts a flexible space before the final action. Use an explicit FlexibleSpace when the
boundary needs to be unambiguous.UIToolbar appearance is retained.For example, this keeps Next and Clear in the left group and starts a separate Done group:
Modifier.inputActions(
InputAction(title = "Next", onClick = { /* ... */ }),
InputAction(title = "Clear", onClick = { /* ... */ }),
InputAction.FlexibleSpace(separatesSharedBackground = true),
InputAction(title = "Done", style = InputActionStyle.Done, onClick = { /* ... */ }),
)Use hidesSharedBackground only when the intended result is to remove an item's shared
background entirely.
Determines the visual appearance and behavior of an InputAction.
| Value | Description |
|---|---|
Plain |
Standard, non-emphasized action button style. |
Done |
Emphasized, bold completion action button style. |
Wrapper for platform-native icon references.
data class InputActionIcon(
val systemName: String,
)| Property | Type | Description |
|---|---|---|
systemName |
String |
SF Symbol icon identifier on iOS (e.g., "chevron.down", "chevron.up", "xmark.circle", "checkmark"). |
Wrap your Compose hierarchy with InputActionsHost in commonMain, then attach inputActions modifiers to your text fields:
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.LocalFocusManager
import zone.ien.inputactions.InputAction
import zone.ien.inputactions.InputActionIcon
import zone.ien.inputactions.InputActionStyle
import zone.ien.inputactions.InputActionsHost
import zone.ien.inputactions.inputActions
@Composable
fun RegistrationScreen() {
val nameFocusRequester = remember { FocusRequester() }
val emailFocusRequester = remember { FocusRequester() }
val focusManager = LocalFocusManager.current
var name by remember { mutableStateOf("") }
var email by remember { mutableStateOf("") }
InputActionsHost {
Column {
OutlinedTextField(
value = name,
onValueChange = { name = it },
modifier = Modifier
.focusRequester(nameFocusRequester)
.inputActions(
InputAction(
title = "Next",
icon = InputActionIcon(systemName = "chevron.down"),
onClick = { emailFocusRequester.requestFocus() },
),
InputAction.FlexibleSpace(separatesSharedBackground = true),
InputAction(
title = "Done",
style = InputActionStyle.Done,
onClick = { focusManager.clearFocus() },
),
),
label = { Text("Name") },
)
OutlinedTextField(
value = email,
onValueChange = { email = it },
modifier = Modifier
.focusRequester(emailFocusRequester)
.inputActions(
InputAction(
title = "Previous",
icon = InputActionIcon(systemName = "chevron.up"),
onClick = { nameFocusRequester.requestFocus() },
),
InputAction.FlexibleSpace,
InputAction(
title = "Done",
style = InputActionStyle.Done,
onClick = { focusManager.clearFocus() },
),
),
label = { Text("Email") },
)
}
}
}The repository includes a Kotlin Multiplatform Compose sample application in the example/ directory.
The sample screen in example/composeApp/src/commonMain/kotlin/zone/ien/inputaction/example/App.kt demonstrates standard and separated text actions, a single Done action, standard and pill-style icon actions, and separated icon groups.
To run the sample app:
./gradlew :example:androidApp:installDebugOpen example/iosApp/iosApp.xcodeproj in Xcode and select a simulator or physical target to run iosApp.
./gradlew :example:composeApp:wasmJsBrowserDevelopmentRunInputActionsHost.Copyright (c) 2026. Compose Input Actions project and open source contributors.
Copyright (c) 2026. IENGROUND of IENLAB.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
English | 한국어
A Kotlin Multiplatform (KMP) library that brings native platform text-input actions and accessory toolbars to Compose Multiplatform TextFields.
On iOS, it attaches native UIToolbar keyboard accessory views directly to the active native text responder owned by Compose, while preserving full Compose ownership of your text state and focus lifecycle. On Android, Desktop (JVM), macOS, and Web (Wasm JS), it gracefully passes through (no-op) without altering standard text editing behavior, ensuring full cross-platform compatibility across your shared Compose code.
TextField using Modifier.inputActions(...).InputAction.FlexibleSpace).InputActionsHost inside commonMain for seamless multiplatform deployment.| Feature | iOS | Android | Desktop (JVM) | macOS | Web (Wasm JS) | Completion Rate | Under the Hood |
|---|---|---|---|---|---|---|---|
| InputActionsHost | 🟢 Yes | 🟢 Yes | 🟢 Yes | 🟢 Yes | 🟢 Yes | 100% | Common CompositionLocal host providing active action registries |
| Modifier.inputActions | 🟢 Yes | 🟢 Yes | 🟢 Yes | 🟢 Yes | 🟢 Yes | 100% | Modifier Node listening to FocusEventModifierNode events |
| Plain & Emphasized Actions | 🟢 Yes | 🟡 No-op | 🟡 No-op | 🟡 No-op | 🟡 No-op | 95% | Native UIBarButtonItem with Plain / Done styles on iOS |
| Icon Actions (SF Symbols) | 🟢 Yes | 🟡 No-op | 🟡 No-op | 🟡 No-op | 🟡 No-op | 95% |
UIImage.systemImageNamed(...) on iOS |
| Flexible Spacers | 🟢 Yes | 🟡 No-op | 🟡 No-op | 🟡 No-op | 🟡 No-op | 100% |
UIBarButtonItem(barButtonSystemItem: .flexibleSpace) on iOS |
| Dynamic Action Updates | 🟢 Yes | 🟢 Yes | 🟢 Yes | 🟢 Yes | 🟢 Yes | 90% | Real-time toolbar item array updates during focus |
Add the dependency to your shared Kotlin Multiplatform module's build.gradle.kts:
kotlin {
sourceSets {
commonMain.dependencies {
implementation("zone.ien.inputactions:inputactions:0.9.0-alpha02")
}
}
}Wrap your Compose UI tree inside your shared commonMain code with InputActionsHost:
// commonMain/kotlin/App.kt
import zone.ien.inputactions.InputActionsHost
@Composable
fun App() {
MaterialTheme {
InputActionsHost {
MainScreen()
}
}
}InputActionsHost intercepts focus events for fields using Modifier.inputActions(...) and attaches the native UIToolbar to the active iOS keyboard responder.InputActionsHost acts as a seamless container (no-op), allowing each platform's standard text-input behavior to operate without additional layout configuration.[!IMPORTANT] Toolchain Requirements: Built and tested with Kotlin 2.4.20-RC2, Compose Multiplatform 1.12.0, and JDK 17. Android targets use minSdk 29 and compileSdk/targetSdk 37.
A wrapper @Composable component that establishes the local action registry host via CompositionLocal.
| Parameter | Type | Default | Description |
|---|---|---|---|
content |
@Composable () -> Unit |
Required | The sub-tree containing TextField components configured with inputActions. |
Modifier extension for Compose text fields to register actions triggered when the field gains focus.
| Parameter | Type | Default | Description |
|---|---|---|---|
vararg actions |
InputAction |
Required | Vararg list of InputAction instances presented in the native keyboard toolbar. |
Represents an action item or spacer displayed in the keyboard accessory toolbar.
class InputAction(
val title: String = "",
val style: InputActionStyle = InputActionStyle.Plain,
val icon: InputActionIcon? = null,
val hidesSharedBackground: Boolean = false,
val separatesSharedBackground: Boolean = false,
val onClick: () -> Unit,
)| Property | Type | Default | Description |
|---|---|---|---|
title |
String |
"" |
Text label displayed on the action button. Can be empty for icon-only actions; ignored for FlexibleSpace. |
style |
InputActionStyle |
InputActionStyle.Plain |
Presentation style (Plain or Done). |
isFlexibleSpace |
Boolean |
false |
Indicates whether this item is a flexible spacer (true) or action button (false). |
icon |
InputActionIcon? |
null |
Optional platform icon. Displays an SF Symbol icon when specified on iOS. |
hidesSharedBackground |
Boolean |
false |
Hides the item's shared toolbar background on iOS 26+. This is different from separating two visible groups. |
separatesSharedBackground |
Boolean |
false |
Inserts UIKit's zero-width fixedSpaceItem() on iOS 26+ so adjacent groups keep their own backgrounds. |
onClick |
() -> Unit |
Required | Callback executed when the action button is tapped. Ignored for FlexibleSpace. |
InputAction.FlexibleSpace: Standard flexible spacer dividing buttons.InputAction.FlexibleSpace(separatesSharedBackground: Boolean = false): Flexible spacer with optional system group separation. This uses UIBarButtonItem.fixedSpaceItem() on iOS 26+.separatesSharedBackground keeps both backgrounds visible and adds a system separator between
two groups. It does not create a custom background, blur, or corner treatment.
InputAction, the separator is placed immediately before that action. Put the
option on the first action of the group that should begin after the boundary.InputAction.FlexibleSpace(separatesSharedBackground = true), the flexible gap remains and
the separator is placed after that gap.FlexibleSpace is supplied and there are multiple actions, the library automatically
inserts a flexible space before the final action. Use an explicit FlexibleSpace when the
boundary needs to be unambiguous.UIToolbar appearance is retained.For example, this keeps Next and Clear in the left group and starts a separate Done group:
Modifier.inputActions(
InputAction(title = "Next", onClick = { /* ... */ }),
InputAction(title = "Clear", onClick = { /* ... */ }),
InputAction.FlexibleSpace(separatesSharedBackground = true),
InputAction(title = "Done", style = InputActionStyle.Done, onClick = { /* ... */ }),
)Use hidesSharedBackground only when the intended result is to remove an item's shared
background entirely.
Determines the visual appearance and behavior of an InputAction.
| Value | Description |
|---|---|
Plain |
Standard, non-emphasized action button style. |
Done |
Emphasized, bold completion action button style. |
Wrapper for platform-native icon references.
data class InputActionIcon(
val systemName: String,
)| Property | Type | Description |
|---|---|---|
systemName |
String |
SF Symbol icon identifier on iOS (e.g., "chevron.down", "chevron.up", "xmark.circle", "checkmark"). |
Wrap your Compose hierarchy with InputActionsHost in commonMain, then attach inputActions modifiers to your text fields:
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.LocalFocusManager
import zone.ien.inputactions.InputAction
import zone.ien.inputactions.InputActionIcon
import zone.ien.inputactions.InputActionStyle
import zone.ien.inputactions.InputActionsHost
import zone.ien.inputactions.inputActions
@Composable
fun RegistrationScreen() {
val nameFocusRequester = remember { FocusRequester() }
val emailFocusRequester = remember { FocusRequester() }
val focusManager = LocalFocusManager.current
var name by remember { mutableStateOf("") }
var email by remember { mutableStateOf("") }
InputActionsHost {
Column {
OutlinedTextField(
value = name,
onValueChange = { name = it },
modifier = Modifier
.focusRequester(nameFocusRequester)
.inputActions(
InputAction(
title = "Next",
icon = InputActionIcon(systemName = "chevron.down"),
onClick = { emailFocusRequester.requestFocus() },
),
InputAction.FlexibleSpace(separatesSharedBackground = true),
InputAction(
title = "Done",
style = InputActionStyle.Done,
onClick = { focusManager.clearFocus() },
),
),
label = { Text("Name") },
)
OutlinedTextField(
value = email,
onValueChange = { email = it },
modifier = Modifier
.focusRequester(emailFocusRequester)
.inputActions(
InputAction(
title = "Previous",
icon = InputActionIcon(systemName = "chevron.up"),
onClick = { nameFocusRequester.requestFocus() },
),
InputAction.FlexibleSpace,
InputAction(
title = "Done",
style = InputActionStyle.Done,
onClick = { focusManager.clearFocus() },
),
),
label = { Text("Email") },
)
}
}
}The repository includes a Kotlin Multiplatform Compose sample application in the example/ directory.
The sample screen in example/composeApp/src/commonMain/kotlin/zone/ien/inputaction/example/App.kt demonstrates standard and separated text actions, a single Done action, standard and pill-style icon actions, and separated icon groups.
To run the sample app:
./gradlew :example:androidApp:installDebugOpen example/iosApp/iosApp.xcodeproj in Xcode and select a simulator or physical target to run iosApp.
./gradlew :example:composeApp:wasmJsBrowserDevelopmentRunInputActionsHost.Copyright (c) 2026. Compose Input Actions project and open source contributors.
Copyright (c) 2026. IENGROUND of IENLAB.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.