Sitemap

Mastering Compose API Design with SelectableChipDefaults

3 min readMay 5, 2025

I needed a custom SelectableChip in Jetpack Compose — built it quickly, but the API was messy and hard to reuse.
Then I looked at how Compose itself structures components like
Button, and everything clicked.
This post shares the cleaner, scalable approach I wish I started with.

🧩 The Problem: Too Many Parameters, Not Enough Consistency

I was building a custom SelectableChip in Jetpack Compose.
You know — a simple, tappable chip with a label and optional icon, where the user could select or unselect it.

My first version worked… but the API was a mess:

SelectableChip(
text = "Online",
selected = true,
onClick = {},
backgroundColor = Color.Blue,
contentColor = Color.White,
iconTint = Color.Yellow,
shape = RoundedCornerShape(16.dp),
padding = PaddingValues(horizontal = 12.dp, vertical = 6.dp),
textStyle = MaterialTheme.typography.bodySmall
){

// Impl...

}

It was:

🔧 Verbose — Every usage required a dozen parameters

🎨 Not themed — No MaterialTheme integration

😵‍💫 Inconsistent — Everyone on the team used different styles

🧱 Hard to scale — I couldn’t reuse logic or apply variants cleanly

I knew there had to be a better way.

💡 The Solution: Centralizing Styling with SelectableChipDefaults

Inspired by Compose’s own ButtonDefaults, I created SelectableChipDefaults — a simple object that centralizes all the theming logic.
This pattern:
- Makes your component API cleaner
- Automatically adapts to MaterialTheme (including dark mode)
- Keeps code DRY and scalable
- Provides easy override points for consumers

✅ What’s Inside SelectableChipDefaults

Here’s what I included:

object SelectableChipDefaults {

@Composable
fun chipColors(
selectedContainerColor: Color = MaterialTheme.colorScheme.primaryContainer,
unselectedContainerColor: Color = MaterialTheme.colorScheme.surfaceVariant,
selectedContentColor: Color = MaterialTheme.colorScheme.onPrimaryContainer,
unselectedContentColor: Color = MaterialTheme.colorScheme.onSurface,
selectedIconTint: Color = MaterialTheme.colorScheme.onPrimaryContainer,
unselectedIconTint: Color = MaterialTheme.colorScheme.onSurfaceVariant
): SelectableChipColors = SelectableChipColors(
selectedContainerColor,
unselectedContainerColor,
selectedContentColor,
unselectedContentColor,
selectedIconTint,
unselectedIconTint
)

val shape: Shape
@Composable get() = RoundedCornerShape(50)

val padding: PaddingValues
@Composable get() = PaddingValues(horizontal = 16.dp, vertical = 8.dp)

val iconSize: Dp get() = 18.dp

val textStyle: TextStyle
@Composable get() = MaterialTheme.typography.labelLarge
}

Now instead of passing everything manually, my component uses smart, theme-aware defaults:

SelectableChip(
text = "EPS",
selected = isSelected,
onClick = { isSelected = !isSelected },
icon = Icons.Default.Home
)

And under the hood, the chip is styled consistently using the defaults object.

⚙️ Why This Pattern Works

1. 🔍 Cleaner API
Consumers don’t need to care about 10+ styling knobs unless they really want to override them. You keep the component surface small and easy to understand.

2. 🎨 Auto Theming
By pulling values from MaterialTheme, the chip instantly:
. Adapts to light/dark mode
. Matches the app’s typography and color system
. Respects shape schemes and design tokens
. This makes it design-system–friendly by default.

3. 🧱 Easy to Override
Let’s say you want a custom-colored chip for “Flutter”:

SelectableChip(
text = "Flutter",
selected = isSelected,
onClick = { /* ... */ },
colors = SelectableChipDefaults.chipColors(
selectedContainerColor = Color(0xFF02569B),
selectedContentColor = Color.White
)
)

🧠 Recap: Why Use SelectableChipDefaults

  • 🎯 Centralized styling — All theme values like colors, padding, shape, and text style live in one place.
  • 🧼 Cleaner API — Developers don’t have to pass dozens of parameters every time they use the chip.
  • 🎨 Theme-aware by default — Automatically adapts to light/dark mode using MaterialTheme.
  • 🧩 Customizable — Easy to override just one or two values without rewriting everything.
  • 🛠 Scalable — Works great in teams and design systems; one change updates all usages.
  • 💡 Fewer bugs, more consistency — No copy-pasted styles, no accidental visual mismatches.

🚀 Conclusion

If you’re building reusable components in Compose, start with a Defaults object. It’s one of the simplest and most powerful patterns you can apply:

  • Makes your composables consistent
  • Makes your APIs clean
  • Makes your themes do more work for you

So the next time you write a Card, Chip, or Button, ask yourself:

Can I group these styling values in a Defaults object?

Your future self — and your team — will thank you.

🧩 Full Source Code

You can find the complete source code for the SelectableChip component and previews on GitHub.

--

--

Eslam Hussein
Eslam Hussein

Written by Eslam Hussein

Senior Android Engineer 🚀 | Android Apps & Automotive 🚗 | Jetpack Compose 💙 | Kotlin & Clean Architecture 🧠 | Based in Munich 🇩🇪

Responses (2)