Jetpack Compose Chip

Chips are compact, rounded UI elements that punch well above their size. They can represent a filter the user has toggled on, a tag they've added, a smart action the system is offering, or an AI-generated suggestion. The Jetpack Compose Chip system gives you four production-ready components out of the box — all built on Material 3 — so you don't have to build any of this from scratch.

In this tutorial you'll work through each Jetpack Compose Chip variant one by one. For each chip type you'll learn what it does, when to use it, see a runnable code example, and understand what happens on screen when the user interacts with it. By the end you'll have a complete working screen that combines all four chip types in a realistic layout.

The Four Chip Types and When to Use Each

The Material 3 chip specification defines four distinct chip semantics. Jetpack Compose maps each one to its own composable:

  • AssistChip — contextual smart actions ("Set a reminder", "Share", "Call back")
  • FilterChip — toggleable selections for filtering or categorizing content
  • InputChip — a piece of user-entered data that can be removed, like an email address or search tag
  • SuggestionChip — system or AI-generated hints the user can accept

Each Jetpack Compose Chip variant shares the same visual shell — a rounded container, optional leading and trailing icons, and a text label — but they have different interaction models and semantics. Mixing them up will confuse users, so understanding the difference before you write a line of code matters.

Setting Up Material 3 Dependencies

Before you can use any Compose Chip component, you need the Material 3 Compose library. Add these to your app-level build.gradle.kts:

dependencies {
    implementation("androidx.compose.material3:material3:1.3.1")
    implementation("androidx.compose.material:material-icons-extended:1.7.6")
}

Wrap your top-level composable in a MaterialTheme so chips inherit your app's color scheme automatically:

import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview

@Preview(showBackground = true)
@Composable
fun AppRoot() {
    MaterialTheme {
        // Chip composables go here
    }
}

Without MaterialTheme, Material 3 components will fall back to a default color scheme. You typically already have this in your app's MainActivity or NavHost, so in most projects this is already taken care of.

AssistChip in Jetpack Compose

AssistChip is for one-time contextual actions — it does not toggle. When the user taps it, it fires an onClick event and that's it. Think of it like an inline button that's semantically tied to the content around it rather than the overall page flow.

Good candidates for AssistChip include "Add to Calendar", "Share Location", "Navigate Here", or "Set a Reminder". Each of these is a direct action, not a preference setting.

AssistChip accepts an onClick lambda, a label composable, and optional leadingIcon and trailingIcon slots:

import androidx.compose.material3.AssistChip
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Alarm
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview

@Preview(showBackground = true)
@Composable
fun AssistChipDemo() {
    AssistChip(
        onClick = { println("Reminder set for this event") },
        label = { Text("Set Reminder") },
        leadingIcon = {
            Icon(
                imageVector = Icons.Filled.Alarm,
                contentDescription = "Alarm icon"
            )
        }
    )
}

Tapping this chip runs the onClick lambda immediately. The chip does not change appearance after tapping — it's not a toggle. The leadingIcon slot takes any composable, so you can pass a Material icon, a custom painter, or even a small image composable.

Reminder set for this event

On screen: a small rounded chip with an alarm bell icon on the left and "Set Reminder" text. Tapping it fires the action and the chip stays visually unchanged.

FilterChip Compose: Building Toggleable Selections

FilterChip is the most commonly used Jetpack Compose Chip in real-world apps. It has two states — selected and unselected — and the chip visually communicates which state it's in. When selected, a checkmark appears before the label and the background fills with the primary container color. When unselected, it reverts to a neutral bordered style.

This is the right chip for Compose filter selection scenarios: product category toggles, search refinements, content tag pickers, and any UI where the user is building a set of active preferences.

The key design decision with FilterChip is that you own the state entirely. You pass in the current boolean and handle the toggle yourself:

import androidx.compose.material3.FilterChip
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.tooling.preview.Preview

@Preview(showBackground = true)
@Composable
fun FilterChipDemo() {
    var isActive by remember { mutableStateOf(false) }

    FilterChip(
        selected = isActive,
        onClick = { isActive = !isActive },
        label = { Text("Free Shipping") }
    )
}

On first render, isActive is false — the chip shows just a border and label. Tap it: isActive becomes true, a checkmark appears, and the background changes. Tap again: back to the neutral state.

A Scrollable Chip Group with Multiple FilterChips

In production you'll almost always have more than one filter chip. Here's how to build a horizontal chip group in Jetpack Compose where each chip independently tracks its own selected state:

import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material3.FilterChip
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateSetOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp

@Preview(showBackground = true)
@Composable
fun FilterChipGroupDemo() {
    val categories = listOf("Android", "Kotlin", "Compose", "Material 3", "Jetpack")
    val selected = remember { mutableStateSetOf<String>() }

    Row(
        horizontalArrangement = Arrangement.spacedBy(8.dp),
        modifier = Modifier
            .horizontalScroll(rememberScrollState())
            .padding(horizontal = 16.dp, vertical = 8.dp)
    ) {
        categories.forEach { category ->
            FilterChip(
                selected = category in selected,
                onClick = {
                    if (category in selected) selected.remove(category)
                    else selected.add(category)
                },
                label = { Text(category) }
            )
        }
    }
}

Using mutableStateSetOf is cleaner than a list here because set membership checks are O(1) and you can't accidentally add duplicates. The horizontalScroll modifier makes the row swipeable when chips overflow the screen — which is standard chip group behavior on Android.

Each chip independently re-composes only when its own selected state changes. The rest of the chips don't re-render, which is efficient for large chip groups.

InputChip Compose Android: Removable User Inputs

The InputChip Compose Android pattern is for content that the user has explicitly created — an email address they typed into a recipient field, a tag they added to a post, a contact they selected from a picker. The chip represents their input and should always have a way to remove it.

InputChip stays in a selected-looking state by design (the user actively added it). You give it a trailing close icon that, when tapped, removes the chip from your data model:

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.size
import androidx.compose.material3.InputChip
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Person
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp

@Preview(showBackground = true)
@Composable
fun InputChipDemo() {
    var visible by remember { mutableStateOf(true) }

    if (visible) {
        InputChip(
            selected = true,
            onClick = {},
            label = { Text("[email protected]") },
            leadingIcon = {
                Icon(
                    imageVector = Icons.Filled.Person,
                    contentDescription = "Contact",
                    modifier = Modifier.size(18.dp)
                )
            },
            trailingIcon = {
                Icon(
                    imageVector = Icons.Filled.Close,
                    contentDescription = "Remove contact",
                    modifier = Modifier
                        .size(16.dp)
                        .clickable { visible = false }
                )
            }
        )
    }
}

When the close icon is tapped, visible flips to false and the chip leaves the composition. In a real email composer you'd remove the address from your recipient list instead of toggling a local boolean.

The leadingIcon here shows a person avatar, which is a common pattern for InputChips that represent contacts — it gives the user a visual anchor for what kind of data the chip holds.

SuggestionChip Jetpack Compose: System-Generated Hints

The SuggestionChip Jetpack Compose component is for content your app or AI generates — not content the user created. Smart reply options in a messaging app, autocomplete topics in a search bar, recommended tags based on the content being written — these are all SuggestionChip territory.

The user didn't ask for these chips explicitly; your system is offering them. Tapping one means "yes, I accept this suggestion":

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.SuggestionChip
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.foundation.layout.padding
import androidx.compose.ui.Modifier

@Preview(showBackground = true)
@Composable
fun SuggestionChipRowDemo() {
    val suggestions = listOf("Coroutines", "Flow", "Compose State")

    Row(
        horizontalArrangement = Arrangement.spacedBy(8.dp),
        modifier = Modifier.padding(8.dp)
    ) {
        suggestions.forEach { suggestion ->
            SuggestionChip(
                onClick = { println("Accepted: $suggestion") },
                label = { Text(suggestion) }
            )
        }
    }
}
Accepted: Coroutines

The chip renders without a selected state and without a default icon — it's intentionally minimal so it reads as a system offer rather than a user preference. Tapping it fires onClick and your app can fill a search field, insert text, or navigate to the suggested topic.

Customizing Compose Chip Colors

Every Jetpack Compose Chip variant accepts a colors parameter that lets you override individual color roles without touching your MaterialTheme. Each chip type ships with a companion defaults object that has a factory method returning the full color configuration.

You only override what you want to change — everything else falls back to the theme:

import androidx.compose.material3.FilterChip
import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview

@Preview(showBackground = true)
@Composable
fun BrandedFilterChipDemo() {
    var following by remember { mutableStateOf(false) }

    FilterChip(
        selected = following,
        onClick = { following = !following },
        label = { Text(if (following) "Following" else "Follow") },
        colors = FilterChipDefaults.filterChipColors(
            selectedContainerColor = Color(0xFF1A73E8),
            selectedLabelColor = Color.White
        )
    )
}

When selected, the chip fills with a blue brand color and shows white text. When unselected, it uses the default neutral style. The same pattern applies to AssistChipDefaults, InputChipDefaults, and SuggestionChipDefaults — they all share the same API shape.

The Disabled State

Every Jetpack Compose Chip type supports an enabled parameter. Setting it to false renders the chip in a muted style with reduced opacity — it does not respond to taps and its colors automatically shift to the disabled palette:

import androidx.compose.material3.AssistChip
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview

@Preview(showBackground = true)
@Composable
fun DisabledChipDemo() {
    AssistChip(
        onClick = {},
        label = { Text("Upgrade Required") },
        enabled = false
    )
}

Use disabled chips when an action exists conceptually but isn't available yet — a feature gated behind a subscription tier, a submit action waiting for form completion, or an option that depends on a previous selection. The chip communicates "this is a thing, but not right now" without hiding it entirely.

Complete Working Example

Here's a full self-contained screen that combines all four Jetpack Compose Chip types in a realistic layout — a content discovery screen with smart actions, filter chips, user tags, and topic suggestions:

import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material3.AssistChip
import androidx.compose.material3.FilterChip
import androidx.compose.material3.Icon
import androidx.compose.material3.InputChip
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SuggestionChip
import androidx.compose.material3.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.BookmarkAdd
import androidx.compose.material.icons.filled.Close
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateSetOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.tooling.preview.Preview

@Preview(showBackground = true)
@Composable
fun ChipShowcaseScreen() {
    val languageFilters = listOf("Kotlin", "Java", "Python", "Swift", "Go")
    val activeFilters = remember { mutableStateSetOf<String>() }

    val userTags = remember { mutableStateListOf("compose", "material3", "android") }

    val suggestions = listOf("State Hoisting", "Side Effects", "Recomposition")

    MaterialTheme {
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(16.dp)
        ) {

            // --- AssistChip section ---
            Text("Quick Action", style = MaterialTheme.typography.labelLarge)
            Spacer(modifier = Modifier.height(8.dp))
            AssistChip(
                onClick = { println("Article saved to reading list") },
                label = { Text("Save Article") },
                leadingIcon = {
                    Icon(
                        imageVector = Icons.Filled.BookmarkAdd,
                        contentDescription = "Save"
                    )
                }
            )

            Spacer(modifier = Modifier.height(24.dp))

            // --- FilterChip section ---
            Text("Filter by Language", style = MaterialTheme.typography.labelLarge)
            Spacer(modifier = Modifier.height(8.dp))
            Row(
                horizontalArrangement = Arrangement.spacedBy(8.dp),
                modifier = Modifier.horizontalScroll(rememberScrollState())
            ) {
                languageFilters.forEach { lang ->
                    FilterChip(
                        selected = lang in activeFilters,
                        onClick = {
                            if (lang in activeFilters) activeFilters.remove(lang)
                            else activeFilters.add(lang)
                        },
                        label = { Text(lang) }
                    )
                }
            }

            Spacer(modifier = Modifier.height(24.dp))

            // --- InputChip section ---
            Text("Your Tags", style = MaterialTheme.typography.labelLarge)
            Spacer(modifier = Modifier.height(8.dp))
            Row(
                horizontalArrangement = Arrangement.spacedBy(8.dp),
                modifier = Modifier.horizontalScroll(rememberScrollState())
            ) {
                userTags.toList().forEach { tag ->
                    InputChip(
                        selected = true,
                        onClick = {},
                        label = { Text(tag) },
                        trailingIcon = {
                            Icon(
                                imageVector = Icons.Filled.Close,
                                contentDescription = "Remove $tag",
                                modifier = Modifier
                                    .size(14.dp)
                                    .clickable { userTags.remove(tag) }
                            )
                        }
                    )
                }
            }

            Spacer(modifier = Modifier.height(24.dp))

            // --- SuggestionChip section ---
            Text("Explore Topics", style = MaterialTheme.typography.labelLarge)
            Spacer(modifier = Modifier.height(8.dp))
            Row(
                horizontalArrangement = Arrangement.spacedBy(8.dp),
                modifier = Modifier.horizontalScroll(rememberScrollState())
            ) {
                suggestions.forEach { topic ->
                    SuggestionChip(
                        onClick = { println("Navigating to: $topic") },
                        label = { Text(topic) }
                    )
                }
            }
        }
    }
}