Chips are compact components that represent input, attributes, or actions. They allow users to enter information, make selections, filter content, or trigger actions. Lumo UI provides three chip variants: filled, elevated, and outlined.
Usage
import com.nomanr.lumo.ui.components.Chip
import com.nomanr.lumo.ui.components.ElevatedChip
import com.nomanr.lumo.ui.components.OutlinedChip
// Basic chip
Chip(
onClick = { },
label = { Text("Chip") }
)
// With icons
Chip(
onClick = { },
leadingIcon = { Icon(Icons.Default.Tag, null) },
label = { Text("Tagged") }
)
Variants
Filled Chip
Default chip with solid background:
Chip(
onClick = { },
label = { Text("Filled Chip") }
)
// With selection state
var selected by remember { mutableStateOf(false) }
Chip(
onClick = { selected = !selected },
selected = selected,
label = { Text("Selectable") }
)
Elevated Chip
Chip with shadow elevation:
ElevatedChip(
onClick = { },
label = { Text("Elevated Chip") }
)
// Selected state
ElevatedChip(
onClick = { },
selected = true,
label = { Text("Selected") }
)
Outlined Chip
Chip with border and transparent background:
OutlinedChip(
onClick = { },
label = { Text("Outlined Chip") }
)
// With icon
OutlinedChip(
onClick = { },
leadingIcon = {
Icon(Icons.Default.Filter, contentDescription = null)
},
label = { Text("Filter") }
)
Icons
Leading Icon
Icon at the start of the chip:
Chip(
onClick = { },
leadingIcon = {
Icon(
imageVector = Icons.Default.Person,
contentDescription = null,
modifier = Modifier.size(16.dp)
)
},
label = { Text("John Doe") }
)
Trailing Icon
Icon at the end of the chip:
Chip(
onClick = { },
label = { Text("Removable") },
trailingIcon = {
Icon(
imageVector = Icons.Default.Close,
contentDescription = "Remove",
modifier = Modifier
.size(16.dp)
.clickable { /* Handle remove */ }
)
}
)
Both Icons
Chip(
onClick = { },
leadingIcon = {
Icon(Icons.Default.Check, null)
},
label = { Text("Selected") },
trailingIcon = {
Icon(Icons.Default.Close, "Remove")
}
)
Selection State
Toggle Selection
var isSelected by remember { mutableStateOf(false) }
Chip(
onClick = { isSelected = !isSelected },
selected = isSelected,
leadingIcon = if (isSelected) {
{ Icon(Icons.Default.Check, null) }
} else {
null
},
label = { Text("Option") }
)
Multi-Select Chip Group
val options = listOf("Small", "Medium", "Large", "X-Large")
var selectedOptions by remember { mutableStateOf(setOf<String>()) }
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
options.forEach { option ->
Chip(
onClick = {
selectedOptions = if (option in selectedOptions) {
selectedOptions - option
} else {
selectedOptions + option
}
},
selected = option in selectedOptions,
label = { Text(option) }
)
}
}
Single-Select Chip Group
val categories = listOf("All", "Electronics", "Clothing", "Books")
var selected by remember { mutableStateOf("All") }
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
categories.forEach { category ->
OutlinedChip(
onClick = { selected = category },
selected = selected == category,
label = { Text(category) }
)
}
}
Removable Chips
Filter Chips with Remove
var filters by remember { mutableStateOf(listOf("Red", "Blue", "Green")) }
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
filters.forEach { filter ->
Chip(
onClick = { },
label = { Text(filter) },
trailingIcon = {
IconButton(
onClick = { filters = filters - filter },
modifier = Modifier.size(18.dp)
) {
Icon(
Icons.Default.Close,
contentDescription = "Remove $filter",
modifier = Modifier.size(14.dp)
)
}
}
)
}
}
Parameters
Chip / ElevatedChip / OutlinedChip
modifier
Modifier
default:"Modifier"
Modifier for the chip
Whether the chip is enabled and clickable
Whether the chip is in a selected state
Callback when the chip is clicked
contentPadding
PaddingValues
default:"PaddingValues(horizontal=6.dp, vertical=6.dp)"
Padding around the chip content
shape
Shape
default:"RoundedCornerShape(12.dp)"
The shape of the chip container
Interaction source for tracking user interactions
leadingIcon
@Composable (() -> Unit)?
default:"null"
Optional icon at the start of the chip
trailingIcon
@Composable (() -> Unit)?
default:"null"
Optional icon at the end of the chip
label
@Composable () -> Unit
required
The text label content of the chip
Use Cases
Tag Selection
val tags = listOf("Android", "iOS", "Web", "Desktop")
var selectedTags by remember { mutableStateOf(setOf<String>()) }
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text("Select Platforms:", style = AppTheme.typography.h4)
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
tags.forEach { tag ->
OutlinedChip(
onClick = {
selectedTags = if (tag in selectedTags) {
selectedTags - tag
} else {
selectedTags + tag
}
},
selected = tag in selectedTags,
leadingIcon = if (tag in selectedTags) {
{ Icon(Icons.Default.Check, null, modifier = Modifier.size(16.dp)) }
} else null,
label = { Text(tag) }
)
}
}
}
Active Filters Display
var activeFilters by remember {
mutableStateOf(mapOf(
"Color" to "Blue",
"Size" to "Large",
"Brand" to "Nike"
))
}
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.padding(16.dp)
) {
activeFilters.forEach { (category, value) ->
Chip(
onClick = { },
leadingIcon = {
Text("$category:", style = AppTheme.typography.label3)
},
label = { Text(value) },
trailingIcon = {
IconButton(
onClick = { activeFilters = activeFilters - category },
modifier = Modifier.size(18.dp)
) {
Icon(Icons.Default.Close, "Remove filter")
}
}
)
}
if (activeFilters.isNotEmpty()) {
OutlinedChip(
onClick = { activeFilters = emptyMap() },
label = { Text("Clear All") }
)
}
}
val contacts = listOf(
Contact("John", "[email protected]"),
Contact("Jane", "[email protected]")
)
var selectedContacts by remember { mutableStateOf(contacts) }
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
selectedContacts.forEach { contact ->
Chip(
onClick = { },
leadingIcon = {
Box(
modifier = Modifier
.size(24.dp)
.background(AppTheme.colors.primary, CircleShape),
contentAlignment = Alignment.Center
) {
Text(
text = contact.name.first().toString(),
color = AppTheme.colors.onPrimary,
style = AppTheme.typography.label3
)
}
},
label = { Text(contact.name) },
trailingIcon = {
IconButton(
onClick = { selectedContacts = selectedContacts - contact },
modifier = Modifier.size(18.dp)
) {
Icon(Icons.Default.Close, "Remove")
}
}
)
}
}
Defaults
- Shape: RoundedCornerShape(12.dp)
- Padding: Horizontal 6.dp, Vertical 6.dp
- Icon Size: 16.dp
- Icon Spacing: 6.dp from label
- Outline Width: 1.dp (OutlinedChip)
- Elevation: 4.dp (ElevatedChip)
- Typography: AppTheme.typography.label3
Best Practices
- Concise Labels: Keep chip text short and scannable
- Icon Usage: Use icons consistently - both or neither for similar chips
- Color States: Clearly differentiate selected vs unselected states
- Remove Actions: Make it easy to remove chips with a trailing close icon
- Grouping: Use FlowRow to wrap chips naturally
- Touch Targets: Ensure chips are easily tappable (minimum 32dp height)
- Accessibility: Provide clear descriptions for chip actions
- Consistency: Use the same chip variant within a group
Accessibility
- Button role is automatically set for screen readers
- Selected state is communicated to assistive technologies
- Ensure sufficient color contrast for text and icons
- Provide content descriptions for icon-only actions
Source Reference
See the full implementation in Chip.kt:38-62 (Chip), Chip.kt:65-89 (ElevatedChip), and Chip.kt:92-116 (OutlinedChip).