Compose Unstyled

TriStateCheckbox

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

View Markdown
TriStateCheckboxDemo.kt
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.Box
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.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.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.RectangleShape
import androidx.compose.ui.state.ToggleableState
import androidx.compose.ui.tooling.preview.Preview
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.Text
import com.composeunstyled.UnstyledCheckbox
import com.composeunstyled.UnstyledIcon
import com.composeunstyled.UnstyledTriStateCheckbox
import com.composeunstyled.demo.borderToken
import com.composeunstyled.demo.colors
import com.composeunstyled.demo.contentToken
import com.composeunstyled.demo.surfaceToken
import com.composeunstyled.theme.Theme

@Preview
@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
  }

  Box(
    modifier = Modifier.fillMaxSize(),
    contentAlignment = Alignment.Center,
  ) {
    Column(
      modifier = Modifier
        .widthIn(max = 300.dp)
        .fillMaxWidth()
        .padding(16.dp),
      verticalArrangement = Arrangement.spacedBy(12.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(RectangleShape)
              .size(24.dp)
              .background(Theme[colors][surfaceToken], RectangleShape)
              .border(1.dp, Theme[colors][borderToken], RectangleShape),
            indication = LocalIndication.current,
          ) { state ->
            when (state) {
              ToggleableState.On -> UnstyledIcon(
                Lucide.Check,
                contentDescription = null,
                tint = Theme[colors][contentToken],
              )

              ToggleableState.Indeterminate -> UnstyledIcon(
                Lucide.Minus,
                contentDescription = null,
                tint = Theme[colors][contentToken],
              )

              ToggleableState.Off -> Unit
            }
          }

          Spacer(Modifier.width(12.dp))
          Text(
            "Select All",
            color = Theme[colors][contentToken],
          )
        }
      }

      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(RectangleShape)
                .size(24.dp)
                .background(Theme[colors][surfaceToken], RectangleShape)
                .border(1.dp, Theme[colors][borderToken], RectangleShape),
              indication = LocalIndication.current,
            ) {
              UnstyledIcon(
                Lucide.Check,
                contentDescription = null,
                tint = Theme[colors][contentToken],
              )
            }

            Spacer(Modifier.width(12.dp))
            Text(option, color = Theme[colors][contentToken])
          }
        }
      }
    }
  }
}

Installation

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

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
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
enabled Boolean
interactionSource MutableInteractionSource

TriStateCheckboxScope.StateIndicator

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