Overview¶
An unstyled Composable component for Compose Multiplatform that can be used to implement Dropdown Menus with the styling of your choice. Fully accessible, supports keyboard navigation and open/close animations.
Available for Compose Desktop, Compose Web (Js/WASM), Jetpack Compose (Android) and iOS.
Installation¶
repositories {
mavenCentral()
}
dependencies {
implementation("com.composables.ui:menu:1.4.0")
}
Basic Example¶
There are four components that you will use to implement a dropdown: Menu
, MenuButton
, MenuContent
and MenuItem
.
The Menu
wraps the MenuButton
and the MenuContent
components. When the MenuButton
is clicked, the MenuContent
will
be displayed on the screen at the position relative to the Menu
.
The MenuContent
component wraps multiple MenuItem
s. When a MenuItem
is clicked, the menu is dismissed.
Each MenuItem
has a onClick
parameter you can use for interaction purposes.
The menu's dropdown visibility is handled for you thanks to the Menu
's internal state.
val options = listOf("United States", "Greece", "Indonesia", "United Kingdom")
var selected by remember { mutableStateOf(0) }
Column(Modifier.fillMaxSize()) {
Menu(Modifier.align(Alignment.End)) {
MenuButton(Modifier.clip(RoundedCornerShape(6.dp)).border(1.dp, Color(0xFFBDBDBD), RoundedCornerShape(6.dp))) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(horizontal = 14.dp, vertical = 10.dp)
) {
BasicText("Options", style = defaultTextStyle.copy(fontWeight = FontWeight(500)))
Spacer(Modifier.width(4.dp))
Image(ChevronDown, null)
}
}
MenuContent(
modifier = Modifier.width(320.dp).border(1.dp, Color(0xFFE0E0E0), RoundedCornerShape(4.dp))
.background(Color.White).padding(4.dp),
hideTransition = fadeOut()
) {
options.forEachIndexed { index, option ->
MenuItem(
modifier = Modifier.clip(RoundedCornerShape(4.dp)),
onClick = { selected = index }
) {
BasicText(option, modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp, horizontal = 4.dp))
}
}
}
}
BasicText("Selected = ${options[selected]}")
}
Code Examples¶
Expand/Close the Menu programmatically¶
Pass your own MenuState
to the Menu
and change the expanded property according to your needs:
val state = rememberMenuState(expanded = true)
Menu(state = state) {
MenuButton {
BasicText("Toggle the menu")
}
MenuContent {
MenuItem(onClick = { state.expanded = false }) {
BasicText("Close this menu")
}
}
}
Change the alignment of the MenuContent
¶
This option is useful if you want to left align, center align or right align the MenuButton
and the MenuContent
when expanded.
Menu {
MenuButton {
BasicText("Toggle the menu")
}
MenuContent(alignment = Alignment.End) {
MenuItem(onClick = { /* TODO */ }) {
BasicText("Option")
}
}
}
Styling¶
By default, the Menu component comes with no styling. This is by design as it is intended to be used as a building block for your own design system's menus.
The Menu
composable is used as an anchor point. Do not pass any styling to its modifier
. Instead, use its modifier
parameter for anchoring and positioning needs (such as Modifier.align()
).
The MenuButton
is the composable responsible to handle clicking into showing and hiding the dropdown menu.
The following sample shows the minimum setup you need to display something on the screen:
Menu {
MenuButton {
BasicText("Show Options")
}
MenuContent {
MenuItem(onClick = { /* TODO handle click */ }) {
BasicText("Option 1")
}
MenuItem(onClick = { /* TODO handle click */ }) {
BasicText("Option 2")
}
MenuItem(onClick = { /* TODO handle click */ }) {
BasicText("Option 3")
}
}
}
However, the result will not look pretty. The following section goes over how to style each component to achieve the visual results you want.
Styling the Menu Button¶
Pass the desired styling to the MenuButton
's modifier
. Do not pass any padding to it, as the MenuButton
handles
click events internally and this will affect the interaction bounds.
Instead, provide any content padding to the contents of the button instead:
Menu {
MenuButton(Modifier.clip(RoundedCornerShape(6.dp)).border(1.dp, Color(0xFFBDBDBD), RoundedCornerShape(6.dp))) {
BasicText("Options", modifier = Modifier.padding(vertical = 8.dp, horizontal = 4.dp))
}
MenuContent {
MenuItem(onClick = { /* TODO handle click */ }) {
BasicText("Option 1")
}
MenuItem(onClick = { /* TODO handle click */ }) {
BasicText("Option 2")
}
MenuItem(onClick = { /* TODO handle click */ }) {
BasicText("Option 3")
}
}
}
Styling the MenuContent¶
The MenuContent
component is a layout on which the menu's items will be displayed when the menu is expanded. In Material
Design this is often a card.
Menu {
MenuButton(Modifier.clip(RoundedCornerShape(6.dp)).border(1.dp, Color(0xFFBDBDBD), RoundedCornerShape(6.dp))) {
BasicText("Options", modifier = Modifier.padding(vertical = 8.dp, horizontal = 4.dp))
}
MenuContent(Modifier.width(320.dp).border(1.dp, Color(0xFFE0E0E0), RoundedCornerShape(4.dp)).background(Color.White).padding(4.dp)) {
MenuItem(onClick = { selected = index }) {
Text(option, modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp, horizontal = 4.dp))
}
}
}
Animating the Menu¶
Modify the showTransition
and hideTransition
parameters of the MenuContent
component to modify the animation specs of the dropdown menu
when it is visible/hidden.
The MenuContent
use the AnimatedVisiblity
composable internally, which gives you a lot of flexibility towards what you
can achieve.
Animation Recipes¶
Material Design Dropdown Animation¶
Material Design scales and fades the dropdown in and out.
MenuContent(
modifier = Modifier.width(320.dp).border(1.dp, Color(0xFFE0E0E0), RoundedCornerShape(4.dp)).background(Color.White).padding(4.dp),
showTransition = scaleIn(tween(durationMillis = 120, easing = LinearOutSlowInEasing), initialScale = 0.8f, transformOrigin = TransformOrigin(0f, 0f)) + fadeIn(tween(durationMillis = 30)),
hideTransition = scaleOut(tween(durationMillis = 1, delayMillis = 75), targetScale = 1f) + fadeOut(tween(durationMillis = 75))
) {
MenuItem(onClick = { /* TODO */ }) {
Basictext("Option 1")
}
MenuItem(onClick = { /* TODO */ }) {
Basictext("Option 2")
}
}
Mac OS Menu Animations¶
macOS shows the menu instantly on click, and quickly fades the menu out when dismissed:
MenuContent(hideTransition = fadeOut(tween(durationMillis = 100, easing = LinearEasing))) {
MenuItem(onClick = { /* TODO */ }) {
Basictext("Option 1")
}
}
Styling touch presses and focus¶
MenuItem
's uses the default Compose mechanism for providing touch and focus feedback. Use the LocalIndication
CompositionLocal to override the default indication.
Here is an example of using Material Design's signature ripple feedback with your menu:
import androidx.compose.foundation.LocalIndication
import androidx.compose.material.ripple.rememberRipple
CompositionLocalProvider(LocalIndication provides rememberRipple()) {
// MenuButton and MenuContent will use the ripple effect when focused and pressed
Menu {
// TODO implement the rest of the menu
}
}
Keyboard Interactions¶
Key | Description |
---|---|
Enter |
Opens the Menu, if the MenuButton is focused. Performs a click, when a MenuItem is focused. |
⬇ |
Opens the Menu, if the MenuButton is focused. Moves focus to the next MenuItem if the Menu is expanded. |
⬆ |
Moves focus to the previous MenuItem if the Menu is expanded. |
Esc |
Closes the Menu, if the Menu is expanded and moves focus to the MenuButton . Removes focus if the MenuButton is focused. |