Skip to main content
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
enabled
Boolean
default:"true"
Whether the chip is enabled and clickable
selected
Boolean
default:"false"
Whether the chip is in a selected state
onClick
() -> Unit
default:"{}"
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
interactionSource
MutableInteractionSource
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") }
        )
    }
}

Contact Chips

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

  1. Concise Labels: Keep chip text short and scannable
  2. Icon Usage: Use icons consistently - both or neither for similar chips
  3. Color States: Clearly differentiate selected vs unselected states
  4. Remove Actions: Make it easy to remove chips with a trailing close icon
  5. Grouping: Use FlowRow to wrap chips naturally
  6. Touch Targets: Ensure chips are easily tappable (minimum 32dp height)
  7. Accessibility: Provide clear descriptions for chip actions
  8. 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).

Build docs developers (and LLMs) love