Sliders & Chips

Filtering the recipe list by a continuous value, like prep time, doesn't fit checkboxes or radio buttons well — a Slider lets the user drag to a value along a range instead of picking from discrete rows. Filtering by category, on the other hand, works nicely as a horizontal row of small, tappable pills — Material 3's chips. This lesson adds both to the bottom sheet built two lessons ago.

Slider: A Continuous Value

Slider reports a plain Float through onValueChange, so a "max prep time" filter needs a small conversion between Float and the Int minutes RecipeBox actually stores:

var maxPrepMinutes by remember { mutableStateOf(60f) }

Column {
    Text("Max prep time: ${maxPrepMinutes.toInt()} min")
    Slider(
        value = maxPrepMinutes,
        onValueChange = { maxPrepMinutes = it },
        valueRange = 5f..120f,
        steps = 22 // snaps to 5-minute increments across the range
    )
}

steps counts the number of discrete stops between the two ends of valueRange, not including the endpoints themselves — here, 22 intermediate stops across a 115-minute range works out to roughly 5-minute increments. Leaving steps unset (or 0) makes the slider fully continuous instead.

FilterChip: Toggleable Category Filters

A FilterChip is a small, pill-shaped toggle — visually distinct when selected — perfect for "show only these categories" style filtering, where more than one chip can be active simultaneously:

var selectedCategories by remember { mutableStateOf(setOf<RecipeCategory>()) }

Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
    RecipeCategory.values().forEach { category ->
        val isSelected = selectedCategories.contains(category)
        FilterChip(
            selected = isSelected,
            onClick = {
                selectedCategories = if (isSelected) {
                    selectedCategories - category
                } else {
                    selectedCategories + category
                }
            },
            label = { Text(category.label) },
            leadingIcon = if (isSelected) {
                { Icon(Icons.Filled.Check, contentDescription = null, modifier = Modifier.size(18.dp)) }
            } else null
        )
    }
}

This renders exactly four chips — Breakfast, Lunch, Dinner, Dessert — each independently toggleable, and leadingIcon conditionally shows a small checkmark only on the chips currently selected.

AssistChip: A Suggested, One-Shot Action

Where FilterChip holds a persistent selected/unselected state, an AssistChip is stateless — it's a compact button for a single suggested action, like offering to clear every active filter at once:

AssistChip(
    onClick = {
        selectedCategories = emptySet()
        maxPrepMinutes = 120f
    },
    label = { Text("Clear filters") },
    leadingIcon = {
        Icon(Icons.Filled.Clear, contentDescription = null, modifier = Modifier.size(18.dp))
    }
)

FilterChip vs. AssistChip vs. RadioButton

It's worth being deliberate here: a row of FilterChips and a column of RadioButtons can look similar in a mockup, but they mean different things — FilterChip naturally supports multiple simultaneous selections (any combination of categories), while RadioButton, as covered last lesson, enforces exactly one. If RecipeBox's filter sheet only ever allowed a single category at a time, radio buttons would actually be the more honest control despite chips being visually trendier.

Note: This is correct, real Compose/Kotlin code — but this website's own project doesn't run a live Android Studio build, so you won't see it actually render on this page.
Next up: Custom Shapes covers RoundedCornerShape, CircleShape, and fully custom polygons — including a category badge that reuses the same category data these chips filter by.