Compose Unstyled 2.0 is out! Check the official announcement blog ->

TriStateCheckbox

A three-state checkbox component for checked, unchecked, and indeterminate values.

import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.background
import androidx.compose.foundation.border
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.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.state.ToggleableState
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import com.composables.icons.lucide.Check
import com.composables.icons.lucide.Lucide
import com.composables.icons.lucide.Minus
import com.composeunstyled.CheckedIndicator
import com.composeunstyled.StateIndicator
import com.composeunstyled.UnstyledCheckbox
import com.composeunstyled.UnstyledIcon
import com.composeunstyled.UnstyledTriStateCheckbox

@Composable
fun TriStateCheckboxDemo() {
  val checkboxOptions = listOf("Option 1", "Option 2", "Option 3", "Option 4")
  var selected by remember { mutableStateOf(listOf(true, true, false, false)) }

  val triState = when {
    selected.all { it } -> ToggleableState.On
    selected.none { it } -> ToggleableState.Off
    else -> ToggleableState.Indeterminate
  }

  Column(
    modifier = Modifier
      .widthIn(max = 300.dp)
      .fillMaxWidth()
      .padding(16.dp),
    verticalArrangement = Arrangement.spacedBy(12.dp),
  ) {
    val triStateShape = RoundedCornerShape(4.dp)
    UnstyledTriStateCheckbox(
      value = triState,
      onClick = {
        val newState = when (triState) {
          ToggleableState.Off -> true
          ToggleableState.Indeterminate -> true
          ToggleableState.On -> false
        }
        selected = List(checkboxOptions.size) { newState }
      },
      modifier = Modifier.fillMaxWidth(),
      accessibilityLabel = "Select all options",
      indication = null,
    ) {
      Row(
        modifier = Modifier.fillMaxWidth(),
        verticalAlignment = Alignment.CenterVertically,
      ) {
        StateIndicator(
          modifier = Modifier
            .clip(triStateShape)
            .size(24.dp)
            .background(Color(0xFFF8FAFC), triStateShape)
            .border(1.dp, Color(0xFFCACACA), triStateShape),
          indication = LocalIndication.current,
        ) { state ->
          when (state) {
            ToggleableState.On -> UnstyledIcon(
              Lucide.Check,
              contentDescription = null,
              tint = Color.Black,
            )

            ToggleableState.Indeterminate -> UnstyledIcon(
              Lucide.Minus,
              contentDescription = null,
              tint = Color.Black,
            )

            ToggleableState.Off -> Unit
          }
        }

        Spacer(Modifier.width(12.dp))
        BasicText("Select All", style = TextStyle(color = Color.Black))
      }
    }

    val checkboxShape = RoundedCornerShape(4.dp)
    checkboxOptions.forEachIndexed { index, option ->
      UnstyledCheckbox(
        checked = selected[index],
        onCheckedChange = { checked ->
          selected = selected.toMutableList().apply {
            this[index] = checked
          }
        },
        modifier = Modifier.fillMaxWidth(),
        accessibilityLabel = option,
        indication = null,
      ) {
        Row(
          modifier = Modifier.fillMaxWidth().padding(start = 36.dp),
          verticalAlignment = Alignment.CenterVertically,
        ) {
          CheckedIndicator(
            modifier = Modifier
              .clip(checkboxShape)
              .size(24.dp)
              .background(Color(0xFFF8FAFC), checkboxShape)
              .border(1.dp, Color(0xFFCACACA), checkboxShape),
            indication = LocalIndication.current,
          ) {
            UnstyledIcon(Lucide.Check, contentDescription = null, tint = Color.Black)
          }

          Spacer(Modifier.width(12.dp))
          BasicText(option, style = TextStyle(color = Color.Black))
        }
      }
    }
  }
}

Installation

implementation("com.composables:composeunstyled-tri-state-checkbox")

Anatomy

UnstyledTriStateCheckbox(
  value = value,
  onClick = onClick,
) {
  StateIndicator {
  }
}

Concepts

  • UnstyledTriStateCheckbox represents the tri-state checkbox interaction target.
  • StateIndicator renders content for the current ToggleableState.

Accessibility

Use tri-state checkboxes for parent selection controls where only some child items are selected.

Code Examples

Rendering each checkbox state

Use the StateIndicator component to render content for each ToggleableState:

UnstyledTriStateCheckbox(
  value = value,
  onClick = { toggleParent() },
) {
  StateIndicator { state ->
    when (state) {
      ToggleableState.On -> BasicText("Selected")
      ToggleableState.Off -> BasicText("Not selected")
      ToggleableState.Indeterminate -> BasicText("Partially selected")
    }
  }
}

Building a select-all checkbox

Use the ToggleableState.Indeterminate value when only some items are selected:

val selectedCount = selectedItems.count()
val parentState = when (selectedCount) {
  0 -> ToggleableState.Off
  items.size -> ToggleableState.On
  else -> ToggleableState.Indeterminate
}

UnstyledTriStateCheckbox(
  value = parentState,
  onClick = {
    selectedItems = if (parentState == ToggleableState.On) emptySet() else items.toSet()
  },
) {
  StateIndicator { state ->
    BasicText(state.toString())
  }
}

API Reference

UnstyledTriStateCheckbox

Parameter Type Description
value ToggleableState The current state (ToggleableState.On, ToggleableState.Off, or ToggleableState.Indeterminate)
onClick () -> Unit Callback invoked when the checkbox is clicked
modifier Modifier Modifier to be applied to the checkbox
enabled Boolean Whether the checkbox is enabled for interaction (defaults to true)
interactionSource MutableInteractionSource? MutableInteractionSource for handling interactions
indication Indication? Visual indication for interactions
accessibilityLabel String?
content TriStateCheckboxScope.() -> Unit

TriStateCheckboxScope

Parameter Type Description
value ToggleableState The current state (ToggleableState.On, ToggleableState.Off, or ToggleableState.Indeterminate)
enabled Boolean Whether the checkbox is enabled for interaction (defaults to true)
interactionSource MutableInteractionSource MutableInteractionSource for handling interactions

TriStateCheckboxScope.StateIndicator

Parameter Type Description
modifier Modifier Modifier to be applied to the checkbox
indication Indication? Visual indication for interactions
content (ToggleableState) -> Unit