Mastering Compose API Design with SelectableChipDefaults
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 likeButton
, 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.