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

A checkbox component with full control over the indicator, bounds, and checked animation.

import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
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.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PathFillType
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.StrokeJoin
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp
import com.composeunstyled.CheckedIndicator
import com.composeunstyled.UnstyledCheckbox
import com.composeunstyled.UnstyledIcon

@Composable
fun CheckboxDemo() {
  var checked by remember { mutableStateOf(true) }
  val checkboxShape = RoundedCornerShape(4.dp)
  UnstyledCheckbox(
    checked = checked,
    onCheckedChange = { checked = it },
    modifier = Modifier.clip(checkboxShape),
    accessibilityLabel = "Enable notifications",
    indication = LocalIndication.current,
  ) {
    CheckedIndicator(
      modifier = Modifier
        .size(24.dp)
        .background(Color(0xFFF8FAFC), checkboxShape)
        .border(1.dp, Color(0xFFCACACA), checkboxShape),
      indication = LocalIndication.current,
    ) {
      UnstyledIcon(Check)
    }
  }
}

private val Check: ImageVector
  get() {
    if (_Check != null) {
      return _Check!!
    }
    _Check = ImageVector.Builder(
      name = "Check",
      defaultWidth = 24.dp,
      defaultHeight = 24.dp,
      viewportWidth = 24f,
      viewportHeight = 24f,
    ).apply {
      path(
        fill = null,
        fillAlpha = 1.0f,
        stroke = SolidColor(Color(0xFF000000)),
        strokeAlpha = 1.0f,
        strokeLineWidth = 2f,
        strokeLineCap = StrokeCap.Round,
        strokeLineJoin = StrokeJoin.Round,
        strokeLineMiter = 1.0f,
        pathFillType = PathFillType.NonZero,
      ) {
        moveTo(20f, 6f)
        lineTo(9f, 17f)
        lineToRelative(-5f, -5f)
      }
    }.build()
    return _Check!!
  }

private var _Check: ImageVector? = null

Features

  • Custom interaction bounds
  • Custom checked indicator
  • Animated checked content
  • Accessibility label

Installation

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

Anatomy

UnstyledCheckbox(
  checked = checked,
  onCheckedChange = onCheckedChange,
) {
  CheckedIndicator {
  }
}

Concepts

  • UnstyledCheckbox represents the interactive bounds of the checkbox.
  • CheckedIndicator represents the visible checked state. It automatically shows and hides its content based on the UnstyledCheckbox state.
  • Give CheckedIndicator a fixed size when its content only exists while checked. Without a fixed size, the checkbox can change layout size when the indicator appears or disappears.

Accessibility

Screen readers will automatically read any text placed inside UnstyledCheckbox.

Use the accessibilityLabel parameter when the checkbox has no visible text label.

Code Examples

Making an entire row checkable

Use the UnstyledCheckbox component as the row container to make the full row toggleable. This is useful when the label should also toggle the checkbox:

UnstyledCheckbox(
  checked = checked,
  onCheckedChange = { checked = it },
) {
  Row {
    CheckedIndicator {
      BasicText("✓")
    }

    BasicText("Accept all terms")
  }
}

Creating larger checkbox interaction bounds

Use padding on the CheckedIndicator component to place the visible checkbox inside a larger interaction area. This is useful when the ripple or touch target should be larger than the visible checkbox:

Animating the checked indicator

Use the enter and exit parameters on CheckedIndicator to animate the checked content.

UnstyledCheckbox(
  checked = checked,
  onCheckedChange = { checked = it },
) {
  CheckedIndicator(
    enter = fadeIn(),
    exit = fadeOut(),
  ) {
    BasicText("Selected")
  }
}

Creating a custom checked indicator animation

Use the checked state value to draw your own indicator instead of CheckedIndicator. This is useful when your design system needs a custom checkmark animation:

Labeling an icon-only checkbox

Use the accessibilityLabel parameter when the checkbox content has no text:

UnstyledCheckbox(
  checked = checked,
  onCheckedChange = { checked = it },
  accessibilityLabel = "Enable notifications",
) {
  CheckedIndicator {
    BasicText("✓")
  }
}

API Reference

UnstyledCheckbox

Parameter Type Description
checked Boolean Whether the checkbox is checked.
onCheckedChange (Boolean) -> Unit Callback when the checked state changes.
modifier Modifier Modifier to be applied to the checkbox.
enabled Boolean
interactionSource MutableInteractionSource?
indication Indication?
accessibilityLabel String?
content CheckboxScope.() -> Unit

CheckboxScope.CheckedIndicator

Parameter Type Description
modifier Modifier Modifier to be applied to the checkbox.
indication Indication?
enter EnterTransition
exit ExitTransition
content AnimatedVisibilityScope.() -> Unit