Blog Infos
Author
Published
Topics
, , , ,
Published

Image source: ChatGPT

 

Welcome to Compose Diaries! In this series where I document the wins, struggles, and little discoveries while building my next Android app (coming to the Play Store soon 🎉).

In this first entry, I’ll take you through how I built a custom Floating Action Button (FAB) menu in Jetpack Compose.

I needed a FAB menu because one action wasn’t enough. I wanted multiple related actions grouped in a clean, expressive way. The Material 3 library has an expressive FAB menu, but I ran into quirks (like fixing the empty space between the screen ending because of the rule FAB should always have 16dp margins by default). So instead of wrestling with it, I built my own. I will add the code blocks with that and my own version too!

Here’s how it went.

First of all :

What is a Floating Action Button (FAB) Menu?

FAB menu is a Floating Action Button that, when pressed, expands into a small menu of related actions. Think of it as a “primary action + a few siblings.” It replaces old patterns like the “speed dial” or stacking multiple mini FABs.

Why it exists
  • Expose 2–6 closely related actions without cluttering the screen
  • Keep the main FAB as the entry point; show extras on demand
  • Add expressive motion so the relationship between actions is clear
How it behaves
  • Trigger: single FAB toggles the menu (open/close)
  • Items: small FABs or extended FABs with labels (tap → action, then close)
  • Placement: bottom-end by default; respects margins (see below)
  • Motion: FAB icon often morphs to “close”; children fade/translate in
  • Dismiss: tapping outside or pressing back closes the menu
  • Accessibility: 48dp touch targets, labels announced, logical focus order
Material spacing rules
  • FAB itself: 16dp margin from screen edges
  • When menu is open:
  • Small/Default FAB → menu bottom margin 16dp
  • Medium FAB → menu bottom margin 40dp
  • Large FAB → menu bottom margin 56dp

Read more about it in here!

For my case I wanted to add a sub two buttons for the screens I want to navigate.

This is the button I wanted to have with two sub-buttons, so I first implemented it as I said, a FloatingActionButtonMenu, but it didn’t work because of the margins. Let’s show you my own version and vice versa.

So first, let’s take a look at the code. In the Scaffold, we have a floatingActionButton, and we can add the button to the Column. After that, we can call the FloatingActionButtonMenu and, inside of it, we can call and fill our FloatingActionButtonMenuItem, and stock them for the number of items we want. Also, on your Scaffold, don’t forget to add:

floatingActionButtonPosition = FabPosition.End
floatingActionButton = {
var expanded by remember { mutableStateOf(false) }
Column(
horizontalAlignment = Alignment.End,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
FloatingActionButtonMenu(
expanded = expanded,
button = {
FloatingActionButton(
containerColor = Color(0xFF54D12E),
contentColor = Color.White,
onClick = { expanded = !expanded },
shape = CircleShape
) {
Row(
modifier = Modifier.padding(horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Icon(
painter = painterResource(R.drawable.ic_plus),
contentDescription = "Write or Reflect Today's Reflection"
)
Text(
text = stringResource(R.string.reflect_button),
modifier = Modifier.padding(start = 8.dp, end = 8.dp)
)
}
}
}
) {

FloatingActionButtonMenuItem(
text = { Text(text = stringResource(R.string.journal)) },
icon = {
Icon(
painter = painterResource(R.drawable.ic_writing),
contentDescription = "Journal"
)
},
containerColor = Color(0xFF2E4229),
contentColor = Color.White,
onClick = onTextJournal
)
FloatingActionButtonMenuItem(
text = { Text(text = stringResource(R.string.record_voice_entry)) },
icon = {
Icon(
painter = painterResource(R.drawable.ic_microphone),
contentDescription = "Voice Record"
)
},
containerColor = Color(0xFF2E4229),
contentColor = Color.White,
onClick = onVoiceJournal
)
}
}

 

As you can see in here we have a bit more margin in the right side of the screen so I don’t want that and configure all of it by myself with AnimatedVisibility. So I build this:

floatingActionButton = {
var expanded by remember { mutableStateOf(false) }
Column(
horizontalAlignment = Alignment.End,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
AnimatedVisibility(visible = expanded) {
Column(
horizontalAlignment = Alignment.End,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Button(
onClick = onTextJournal,
colors = ButtonDefaults.buttonColors(
contentColor = Color.White,
containerColor = Color(0xFF2E4229)
)
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = painterResource(R.drawable.ic_writing),
contentDescription = null
)
Text(text = stringResource(R.string.journal))
}
}
Button(
onClick = onVoiceJournal,
colors = ButtonDefaults.buttonColors(
contentColor = Color.White,
containerColor = Color(0xFF2E4229)
)
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = painterResource(R.drawable.ic_microphone),
contentDescription = null
)
Text(text = stringResource(R.string.record_voice_entry))
}
}
}
}
FloatingActionButton(
containerColor = Color(0xFF54D12E),
contentColor = Color.White,
onClick = { expanded = !expanded },
shape = CircleShape
) {
Row(
modifier = Modifier.padding(horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Icon(
painter = painterResource(R.drawable.ic_plus),
contentDescription = "Write or Reflect Today's Reflection"
)
Text(
text = stringResource(R.string.record_voice_entry),
modifier = Modifier.padding(start = 8.dp, end = 8.dp)
)
}
}
}
}

 

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

,

Compose Beyond Material Design

In the implementation of Jetpack compose at our organization, one of the issues that popped up was how to implement a Design system that is not adapted from material design but true to what our…
Watch Video

Compose Beyond Material Design

Frank Tamre

Compose Beyond Material Design

Frank Tamre

Compose Beyond Material Design

Frank Tamre

Jobs

As a little tip when you pass a custom ButtonColors (i.e., your own implementation), you must define the full state logic for both containerColor,contentColor,disabledContentColor and DisabledContainerColor for enabled and disabled. If you use ButtonDefaults.buttonColors(...), you can specify just a subset (e.g., only containerColor), and the other values default to the theme.

colors = ButtonDefaults.buttonColors(
contentColor = Color.White,
containerColor = Color(0xFF2E4229)
)
colors = ButtonColors(
contentColor = Color.White,
containerColor = Color(0xFF2E4229),
disabledContainerColor = Color(0xFF2E4229),
disabledContentColor = Color.White
)

I hope this series helped you learn some new things about Compose through real, usable examples.

Follow me on Medium, Github or connect on LinkedIn.

This article was previously published on proandroiddev.com

Menu